Skip to content
This repository has been archived by the owner on Jul 12, 2024. It is now read-only.

Commit

Permalink
test: init test container fixture
Browse files Browse the repository at this point in the history
  • Loading branch information
foxminchan committed May 12, 2024
1 parent 8e1c797 commit b1550b8
Show file tree
Hide file tree
Showing 5 changed files with 187 additions and 8 deletions.
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
<!-- xUnit -->
<PackageVersion Include="xunit" Version="2.8.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.0" />
<PackageVersion Include="XunitContext" Version="3.3.2" />
<!-- TestContainers -->
<PackageVersion Include="Testcontainers" Version="$(TestcontainersVersion)" />
<PackageVersion Include="Testcontainers.Redis" Version="$(TestcontainersVersion)" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using Microsoft.Extensions.DependencyInjection;
using RookieShop.IntegrationTests.Fixtures;
using RookieShop.Persistence;

namespace RookieShop.IntegrationTests.Extensions;

public static class ApplicationExtension
{
public static async Task EnsureCreatedAsync<T>(
this ApplicationFactory<T> factory,
CancellationToken cancellationToken = default)
where T : class
{
await using var scope = factory.Instance.Services.CreateAsyncScope();
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
await dbContext.Database.EnsureCreatedAsync(cancellationToken);
}

public static async Task EnsureCreatedAndPopulateDataAsync<TProgram, TEntity>(
this ApplicationFactory<TProgram> factory,
IReadOnlyCollection<TEntity> entities,
CancellationToken cancellationToken = default)
where TProgram : class
where TEntity : class
{
await using var scope = factory.Instance.Services.CreateAsyncScope();
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
await dbContext.Database.EnsureCreatedAsync(cancellationToken);
await dbContext.Set<TEntity>().AddRangeAsync(entities, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using DotNet.Testcontainers.Containers;
using Polly;

namespace RookieShop.IntegrationTests.Extensions;

public static class TestContainersExtension
{
public static Task StartWithWaitAndRetryAsync(
this IContainer container,
int retryCount = 3,
int retryDelay = 3,
CancellationToken cancellationToken = default) =>
Policy
.Handle<AggregateException>()
.Or<InvalidOperationException>()
.WaitAndRetryAsync(retryCount, _ => TimeSpan.FromSeconds(retryCount * retryDelay))
.ExecuteAsync(container.StartAsync, cancellationToken);
}
117 changes: 117 additions & 0 deletions tests/RookieShop.IntegrationTests/Fixtures/ApplicationFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
using System.Diagnostics;
using DotNet.Testcontainers.Containers;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using RookieShop.IntegrationTests.Extensions;
using Testcontainers.Azurite;
using Testcontainers.PostgreSql;
using Testcontainers.Redis;

namespace RookieShop.IntegrationTests.Fixtures;

public sealed class ApplicationFactory<TProgram>
: WebApplicationFactory<TProgram>, IAsyncLifetime, IContextFixture where TProgram : class
{
private readonly List<IContainer> _containers = [];
private AzuriteContainer _storageContainer = default!;
public WebApplicationFactory<TProgram> Instance { get; private set; } = default!;

public Task InitializeAsync()
{
Debug.WriteLine($"{nameof(ApplicationFactory<TProgram>)} called {nameof(InitializeAsync)}");
var env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Test";
Instance = WithWebHostBuilder(builder => builder.UseEnvironment(env));
return Task.CompletedTask;
}

public new Task DisposeAsync()
{
Debug.WriteLine($"{nameof(ApplicationFactory<TProgram>)} called {nameof(DisposeAsync)}");
return Task
.WhenAll(_containers.Select(container => container.DisposeAsync().AsTask()))
.ContinueWith(async _ => await base.DisposeAsync());
}

public ApplicationFactory<TProgram> WithCacheContainer()
{
Debug.WriteLine($"{nameof(ApplicationFactory<TProgram>)} called {nameof(WithCacheContainer)}");
_containers.Add(new RedisBuilder()
.WithName($"test_cache_{Guid.NewGuid()}")
.WithImage("redis:alpine")
.WithCleanUp(true)
.Build());

return this;
}

public ApplicationFactory<TProgram> WithDbContainer()
{
Debug.WriteLine($"{nameof(ApplicationFactory<TProgram>)} called {nameof(WithDbContainer)}");
_containers.Add(new PostgreSqlBuilder()
.WithDatabase($"test_db_{Guid.NewGuid()}")
.WithUsername("postgres")
.WithPassword("postgres")
.WithImage("postgres:alpine")
.WithCleanUp(true)
.Build());

return this;
}

public ApplicationFactory<TProgram> WithStorageContainer()
{
Debug.WriteLine($"{nameof(ApplicationFactory<TProgram>)} called {nameof(WithStorageContainer)}");
_storageContainer = new AzuriteBuilder()
.WithPortBinding(10000, true)
.Build();

return this;
}

public async Task StartContainersAsync(CancellationToken cancellationToken = default)
{
Debug.WriteLine($"{nameof(ApplicationFactory<TProgram>)} called {nameof(StartContainersAsync)}");

await _storageContainer.StartWithWaitAndRetryAsync(cancellationToken: cancellationToken);

if (_containers.Count == 0) return;

await Task.WhenAll(_containers.Select(container =>
container.StartWithWaitAndRetryAsync(cancellationToken: cancellationToken)));

Instance = _containers.Aggregate(this as WebApplicationFactory<TProgram>, (current, container) =>
current.WithWebHostBuilder(builder =>
{
switch (container)
{
case PostgreSqlContainer dbContainer:
builder.UseSetting("ConnectionStrings:Postgres", dbContainer.GetConnectionString());
break;
case RedisContainer cacheContainer:
builder.UseSetting("Redis:Url", cacheContainer.GetConnectionString());
break;
case AzuriteContainer storageContainer:
builder.UseSetting("ConnectionStrings:Azurite", storageContainer.GetConnectionString());
break;
}
}));
}

public new HttpClient CreateClient() => Instance.CreateClient();

public async Task StopContainersAsync()
{
Debug.WriteLine($"{nameof(ApplicationFactory<TProgram>)} called {nameof(StopContainersAsync)}");

if (_containers.Count == 0) return;

await Task.WhenAll(_containers.Select(container => container.DisposeAsync().AsTask()))
.ContinueWith(async _ => await base.DisposeAsync())
.ContinueWith(async _ => await InitializeAsync())
.ConfigureAwait(false);

_containers.Clear();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,26 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="xunit.runner.visualstudio" />
<PackageReference Include="XunitContext" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="FluentAssertions" />
<PackageReference Include="AutoBogus.NSubstitute" />
<PackageReference Include="Polly" />
<PackageReference Include="Ardalis.HttpClientTestExtensions" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Testcontainers" />
<PackageReference Include="Testcontainers.Azurite" />
<PackageReference Include="Testcontainers.PostgreSql" />
<PackageReference Include="Testcontainers.Redis" />
</ItemGroup>

<ItemGroup>
Expand All @@ -32,6 +42,7 @@

<ItemGroup>
<Using Include="Xunit" />
<Using Include="FluentAssertions" />
</ItemGroup>

<ItemGroup>
Expand Down

0 comments on commit b1550b8

Please sign in to comment.