Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Run integration tests against a Postgres container (Testcontainers) #135

Merged
merged 6 commits into from
Mar 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 6 additions & 8 deletions .github/workflows/test-backend.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,9 @@ jobs:
uses: actions/setup-dotnet@v1
with:
dotnet-version: '7.0.x'
- name: Create .env file
run: cp example.env .env
- name: Spin up containers
run: docker compose up -d
- name: Run Tests on In Memory Database
run: ApplicationContext__UseInMemoryDb=true dotnet test LeaderboardBackend.Test
- name: Run Tests on Test Postgres Database
run: ApplicationContext__UseInMemoryDb=false dotnet test LeaderboardBackend.Test
- name: Restore NuGet packages
run: dotnet restore
- name: Build
run: dotnet build --no-restore
- name: Run tests
run: dotnet test LeaderboardBackend.Test --no-restore
2 changes: 1 addition & 1 deletion LeaderboardBackend.Jobs/LeaderboardBackend.Jobs.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
</ItemGroup>

<ItemGroup>
<None Include="../.env">
<None Include="../.env" Condition="Exists('../.env')">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
Expand Down
15 changes: 12 additions & 3 deletions LeaderboardBackend.Test/Bans.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,14 @@ internal class Bans
private static User s_modUser = null!;
private static User s_normalUser = null!;

[SetUp]
public static async Task SetUp()
[OneTimeSetUp]
public async Task OneTimeSetup()
{
s_factory = new TestApiFactory();
s_factory = new();
s_apiClient = s_factory.CreateTestApiClient();

s_factory.ResetDatabase();

s_adminJwt = (await s_apiClient.LoginAdminUser()).Token;

// Set up users and leaderboard
Expand Down Expand Up @@ -61,6 +64,12 @@ await s_apiClient.Post<Modship>(
s_modJwt = (await s_apiClient.LoginUser("[email protected]", "Passw0rd!")).Token;
}

[OneTimeTearDown]
public void OneTimeTeardown()
{
s_factory.Dispose();
}

[Test]
public static async Task CreateSiteBan_Ok()
{
Expand Down
10 changes: 9 additions & 1 deletion LeaderboardBackend.Test/Categories.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,21 @@ internal class Categories
private static string? s_jwt;

[OneTimeSetUp]
public static async Task SetUp()
public async Task OneTimeSetUp()
{
s_factory = new TestApiFactory();
s_apiClient = s_factory.CreateTestApiClient();

s_factory.ResetDatabase();
s_jwt = (await s_apiClient.LoginAdminUser()).Token;
}

[OneTimeTearDown]
public void OneTimeTearDown()
{
s_factory.Dispose();
}

[Test]
public static void GetCategory_Unauthorized()
{
Expand Down
107 changes: 107 additions & 0 deletions LeaderboardBackend.Test/Fixtures/PostgresDatabaseFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
using System;
using System.Threading.Tasks;
using LeaderboardBackend.Test.Fixtures;
using Npgsql;
using NUnit.Framework;
using Testcontainers.PostgreSql;

// Fixtures apply to all tests in its namespace
// It has no namespace on purpose, so that the fixture applies to all tests in this assembly

[SetUpFixture] // https://docs.nunit.org/articles/nunit/writing-tests/attributes/setupfixture.html
internal class PostgresDatabaseFixture
{
public static PostgreSqlContainer? PostgresContainer { get; private set; }
public static string? Database { get; private set; }
public static string? Username { get; private set; }
public static string? Password { get; private set; }
public static int Port { get; private set; }
public static bool HasCreatedTemplate { get; private set; } = false;
private static string TemplateDatabase => Database! + "_template";

[OneTimeSetUp]
public static async Task OneTimeSetup()
{
if (TestConfig.DatabaseBackend != DatabaseBackend.TestContainer)
{
return;
}

PostgresContainer = new PostgreSqlBuilder()
.WithTmpfsMount("/var/lib/postgresql/data") // db files in-memory
zysim marked this conversation as resolved.
Show resolved Hide resolved
.Build();
await PostgresContainer.StartAsync();

NpgsqlConnectionStringBuilder connStrBuilder = new(PostgresContainer.GetConnectionString());
Username = connStrBuilder.Username!;
Password = connStrBuilder.Password!;
Database = connStrBuilder.Database!;
Port = connStrBuilder.Port;
}

[OneTimeTearDown]
public static async Task OneTimeTearDown()
{
if (PostgresContainer is null)
{
return;
}

await PostgresContainer.DisposeAsync();
}

public static void CreateTemplateFromCurrentDb()
{
ThrowIfNotInitialized();

NpgsqlConnection.ClearAllPools(); // can't drop a DB if connections remain open
using NpgsqlDataSource conn = CreateConnectionToTemplate();
conn.CreateCommand(@$"
DROP DATABASE IF EXISTS {TemplateDatabase};
CREATE DATABASE {TemplateDatabase}
WITH TEMPLATE {Database}
OWNER '{Username}';
")
.ExecuteNonQuery();
HasCreatedTemplate = true;
}


// It is faster to recreate the db from an already seeded template
// compared to dropping the db and recreating it from scratch
public static void ResetDatabaseToTemplate()
{
ThrowIfNotInitialized();
if (!HasCreatedTemplate)
{
throw new InvalidOperationException("Database template has not been created.");
}
NpgsqlConnection.ClearAllPools(); // can't drop a DB if connections remain open
using NpgsqlDataSource conn = CreateConnectionToTemplate();
conn.CreateCommand(@$"
DROP DATABASE IF EXISTS {Database};
CREATE DATABASE {Database}
WITH TEMPLATE {TemplateDatabase}
OWNER '{Username}';
")
.ExecuteNonQuery();
}

private static NpgsqlDataSource CreateConnectionToTemplate()
{
ThrowIfNotInitialized();
NpgsqlConnectionStringBuilder connStrBuilder = new(PostgresContainer!.GetConnectionString())
{
Database = "template1"
};
return NpgsqlDataSource.Create(connStrBuilder);
}

private static void ThrowIfNotInitialized()
{
if (PostgresContainer is null)
{
throw new InvalidOperationException("Postgres container is not initialized.");
}
}
}
23 changes: 23 additions & 0 deletions LeaderboardBackend.Test/Fixtures/TestConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System;

namespace LeaderboardBackend.Test.Fixtures;

internal static class TestConfig
{
public static DatabaseBackend DatabaseBackend { get; private set; } = DatabaseBackend.TestContainer;

static TestConfig()
{
string? backendVar = Environment.GetEnvironmentVariable("INTEGRATION_TESTS_DB_BACKEND");
if (Enum.TryParse(backendVar, out DatabaseBackend dbBackend))
{
DatabaseBackend = dbBackend;
}
}
}

internal enum DatabaseBackend
{
InMemory,
TestContainer,
}
12 changes: 10 additions & 2 deletions LeaderboardBackend.Test/Judgements.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,14 @@ internal class Judgements
private const string VALID_PASSWORD = "c00l_pAssword";
private const string VALID_EMAIL = "[email protected]";

[SetUp]
public static async Task SetUp()
[OneTimeSetUp]
public async Task OneTimeSetup()
{
s_factory = new TestApiFactory();
s_apiClient = s_factory.CreateTestApiClient();

s_factory.ResetDatabase();

// Set up a default Leaderboard and a mod user for that leaderboard to use as the Jwt for
// tests
string adminJwt = (await s_apiClient.LoginAdminUser()).Token;
Expand Down Expand Up @@ -77,6 +79,12 @@ public static async Task SetUp()
s_jwt = (await s_apiClient.LoginUser(VALID_EMAIL, VALID_PASSWORD)).Token;
}

[OneTimeTearDown]
public void OneTimeTearDown()
{
s_factory.Dispose();
}

[Test]
public async Task CreateJudgement_OK()
{
Expand Down
4 changes: 3 additions & 1 deletion LeaderboardBackend.Test/LeaderboardBackend.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,16 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Testcontainers" Version="3.0.0" />
<PackageReference Include="Testcontainers.PostgreSql" Version="3.0.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\LeaderboardBackend\LeaderboardBackend.csproj" />
</ItemGroup>

<ItemGroup>
<None Include="../.env">
<None Include="../.env" Condition="Exists('../.env')">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
Expand Down
14 changes: 11 additions & 3 deletions LeaderboardBackend.Test/Leaderboards.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,27 @@ internal class Leaderboards
private static TestApiFactory s_factory = null!;
private static string? s_jwt;

[SetUp]
public static async Task SetUp()
[OneTimeSetUp]
public async Task OneTimeSetUp()
{
s_factory = new TestApiFactory();
s_apiClient = s_factory.CreateTestApiClient();

s_factory.ResetDatabase();
s_jwt = (await s_apiClient.LoginAdminUser()).Token;
}

[OneTimeTearDown]
public void OneTimeTearDown()
{
s_factory.Dispose();
}

[Test]
public static void GetLeaderboard_NotFound()
{
RequestFailureException e = Assert.ThrowsAsync<RequestFailureException>(async () =>
await s_apiClient.Get<Leaderboard>($"/api/leaderboards/2", new()))!;
await s_apiClient.Get<Leaderboard>($"/api/leaderboards/{long.MaxValue}", new()))!;

Assert.AreEqual(HttpStatusCode.NotFound, e.Response.StatusCode);
}
Expand Down
17 changes: 15 additions & 2 deletions LeaderboardBackend.Test/Modships.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,27 @@ internal class Modships
private static TestApiFactory s_factory = null!;
private static string s_jwt = null!;

[SetUp]
public static async Task SetUp()
[OneTimeSetUp]
public async Task OneTimeSetUp()
{
s_factory = new TestApiFactory();
s_apiClient = s_factory.CreateTestApiClient();

s_jwt = (await s_apiClient.LoginAdminUser()).Token;
}

[OneTimeTearDown]
public void OneTimeTearDown()
{
s_factory.Dispose();
}

[SetUp]
public void SetUp()
{
s_factory.ResetDatabase();
}

[Test]
public static async Task CreateModship_OK()
{
Expand Down
11 changes: 9 additions & 2 deletions LeaderboardBackend.Test/Runs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,18 @@ internal class Runs
private static string s_jwt = null!;
private static long s_categoryId;

[SetUp]
public static async Task SetUp()
[OneTimeSetUp]
public void OneTimeSetUp()
{
s_factory = new TestApiFactory();
s_apiClient = s_factory.CreateTestApiClient();
}

[SetUp]
public async Task SetUp()
{
s_factory.ResetDatabase();

s_jwt = (await s_apiClient.LoginAdminUser()).Token;

Leaderboard createdLeaderboard = await s_apiClient.Post<Leaderboard>(
Expand Down
Loading