mirror of
https://github.com/alex289/CleanArchitecture.git
synced 2025-06-29 18:21:08 +00:00
feat: Use TestContainers for integration tests
This commit is contained in:
parent
35d7482ade
commit
2c7748d877
@ -10,10 +10,13 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.4" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="8.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="8.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.4" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
||||
<PackageReference Include="Respawn" Version="6.2.1" />
|
||||
<PackageReference Include="Testcontainers" Version="3.8.0" />
|
||||
<PackageReference Include="Testcontainers.MsSql" Version="3.8.0" />
|
||||
<PackageReference Include="Testcontainers.RabbitMq" Version="3.8.0" />
|
||||
<PackageReference Include="Testcontainers.Redis" Version="3.8.0" />
|
||||
<PackageReference Include="xunit" Version="2.7.0" />
|
||||
<PackageReference Include="Xunit.Priority" Version="1.1.6" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.7">
|
||||
|
@ -1,46 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data.Common;
|
||||
using System.Linq;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace CleanArchitecture.IntegrationTests.Extensions;
|
||||
|
||||
public static class FunctionalTestsServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection SetupTestDatabase<TContext>(this IServiceCollection services,
|
||||
DbConnection connection) where TContext : DbContext
|
||||
{
|
||||
var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions<TContext>));
|
||||
if (descriptor is not null)
|
||||
services.Remove(descriptor);
|
||||
|
||||
services.AddScoped(p =>
|
||||
DbContextOptionsFactory<TContext>(
|
||||
p,
|
||||
(_, options) => options
|
||||
.ConfigureWarnings(b => b.Log(CoreEventId.ManyServiceProvidersCreatedWarning))
|
||||
.UseLazyLoadingProxies()
|
||||
.UseSqlite(connection)));
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static DbContextOptions<TContext> DbContextOptionsFactory<TContext>(
|
||||
IServiceProvider applicationServiceProvider,
|
||||
Action<IServiceProvider, DbContextOptionsBuilder> optionsAction)
|
||||
where TContext : DbContext
|
||||
{
|
||||
var builder = new DbContextOptionsBuilder<TContext>(
|
||||
new DbContextOptions<TContext>(new Dictionary<Type, IDbContextOptionsExtension>()));
|
||||
|
||||
builder.UseApplicationServiceProvider(applicationServiceProvider);
|
||||
|
||||
optionsAction.Invoke(applicationServiceProvider, builder);
|
||||
|
||||
return builder.Options;
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using CleanArchitecture.IntegrationTests.Infrastructure;
|
||||
using Xunit;
|
||||
|
||||
namespace CleanArchitecture.IntegrationTests.Fixtures;
|
||||
|
||||
public sealed class DatabaseFixture : IAsyncLifetime
|
||||
{
|
||||
public static string TestRunDbName { get; } = $"CleanArchitecture-Integration-{Guid.NewGuid()}";
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
var db = DatabaseAccessor.GetOrCreateAsync(TestRunDbName);
|
||||
await db.DisposeAsync();
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
var db = DatabaseAccessor.GetOrCreateAsync(TestRunDbName);
|
||||
await db.InitializeAsync();
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using CleanArchitecture.Domain.Constants;
|
||||
using CleanArchitecture.Domain.Entities;
|
||||
using CleanArchitecture.Domain.Enums;
|
||||
@ -8,20 +9,21 @@ using CleanArchitecture.IntegrationTests.Infrastructure;
|
||||
using CleanArchitecture.IntegrationTests.Infrastructure.Auth;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Xunit;
|
||||
|
||||
namespace CleanArchitecture.IntegrationTests.Fixtures;
|
||||
|
||||
public class TestFixtureBase
|
||||
public class TestFixtureBase : IAsyncLifetime
|
||||
{
|
||||
public HttpClient ServerClient { get; }
|
||||
protected WebApplicationFactory<Program> Factory { get; }
|
||||
protected CleanArchitectureWebApplicationFactory Factory { get; }
|
||||
|
||||
public TestFixtureBase(bool useTestAuthentication = true)
|
||||
{
|
||||
Factory = new CleanArchitectureWebApplicationFactory(
|
||||
SeedTestData,
|
||||
RegisterCustomServicesHandler,
|
||||
useTestAuthentication);
|
||||
useTestAuthentication,
|
||||
DatabaseFixture.TestRunDbName);
|
||||
|
||||
ServerClient = Factory.CreateClient();
|
||||
ServerClient.Timeout = TimeSpan.FromMinutes(5);
|
||||
@ -29,17 +31,17 @@ public class TestFixtureBase
|
||||
|
||||
protected virtual void SeedTestData(ApplicationDbContext context)
|
||||
{
|
||||
context.Users.Add(new User(
|
||||
TestAuthenticationOptions.TestUserId,
|
||||
Ids.Seed.TenantId,
|
||||
TestAuthenticationOptions.Email,
|
||||
TestAuthenticationOptions.FirstName,
|
||||
TestAuthenticationOptions.LastName,
|
||||
// !Password123#
|
||||
"$2a$12$Blal/uiFIJdYsCLTMUik/egLbfg3XhbnxBC6Sb5IKz2ZYhiU/MzL2",
|
||||
UserRole.Admin));
|
||||
}
|
||||
|
||||
context.SaveChanges();
|
||||
private async Task PrepareDatabaseAsync()
|
||||
{
|
||||
await Factory.RespawnDatabaseAsync();
|
||||
|
||||
using var scope = Factory.Services.CreateScope();
|
||||
await using var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
||||
|
||||
SeedTestData(dbContext);
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
protected virtual void RegisterCustomServicesHandler(
|
||||
@ -48,4 +50,14 @@ public class TestFixtureBase
|
||||
IServiceProvider scopedServices)
|
||||
{
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
await PrepareDatabaseAsync();
|
||||
}
|
||||
|
||||
public Task DisposeAsync()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
@ -1,13 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using CleanArchitecture.Infrastructure.Database;
|
||||
using CleanArchitecture.Infrastructure.Extensions;
|
||||
using CleanArchitecture.IntegrationTests.Extensions;
|
||||
using CleanArchitecture.IntegrationTests.Infrastructure.Auth;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace CleanArchitecture.IntegrationTests.Infrastructure;
|
||||
@ -21,20 +19,19 @@ public sealed class CleanArchitectureWebApplicationFactory : WebApplicationFacto
|
||||
ServiceProvider serviceProvider,
|
||||
IServiceProvider scopedServices);
|
||||
|
||||
private readonly AddCustomSeedDataHandler? _addCustomSeedDataHandler;
|
||||
private readonly bool _addTestAuthentication;
|
||||
private readonly string _instanceDatabaseName;
|
||||
|
||||
private readonly SqliteConnection _connection = new("DataSource=:memory:");
|
||||
private readonly bool _addTestAuthentication;
|
||||
private readonly RegisterCustomServicesHandler? _registerCustomServicesHandler;
|
||||
|
||||
public CleanArchitectureWebApplicationFactory(
|
||||
AddCustomSeedDataHandler? addCustomSeedDataHandler,
|
||||
RegisterCustomServicesHandler? registerCustomServicesHandler,
|
||||
bool addTestAuthentication)
|
||||
bool addTestAuthentication,
|
||||
string instanceDatabaseName)
|
||||
{
|
||||
_addCustomSeedDataHandler = addCustomSeedDataHandler;
|
||||
_registerCustomServicesHandler = registerCustomServicesHandler;
|
||||
_addTestAuthentication = addTestAuthentication;
|
||||
_instanceDatabaseName = instanceDatabaseName;
|
||||
}
|
||||
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
@ -43,14 +40,22 @@ public sealed class CleanArchitectureWebApplicationFactory : WebApplicationFacto
|
||||
|
||||
base.ConfigureWebHost(builder);
|
||||
|
||||
_connection.Open();
|
||||
builder.ConfigureAppConfiguration(configurationBuilder =>
|
||||
{
|
||||
configurationBuilder.AddEnvironmentVariables();
|
||||
|
||||
var accessor = DatabaseAccessor.GetOrCreateAsync(_instanceDatabaseName);
|
||||
|
||||
// Overwrite default connection string to our test instance db
|
||||
configurationBuilder.AddInMemoryCollection([
|
||||
new KeyValuePair<string, string?>(
|
||||
"ConnectionStrings:DefaultConnection",
|
||||
accessor.GetConnectionString())
|
||||
]);
|
||||
});
|
||||
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
services.SetupTestDatabase<ApplicationDbContext>(_connection);
|
||||
services.SetupTestDatabase<EventStoreDbContext>(_connection);
|
||||
services.SetupTestDatabase<DomainNotificationStoreDbContext>(_connection);
|
||||
|
||||
if (_addTestAuthentication)
|
||||
{
|
||||
services.AddAuthentication(options =>
|
||||
@ -65,22 +70,22 @@ public sealed class CleanArchitectureWebApplicationFactory : WebApplicationFacto
|
||||
using var scope = sp.CreateScope();
|
||||
var scopedServices = scope.ServiceProvider;
|
||||
|
||||
var applicationDbContext = scopedServices.GetRequiredService<ApplicationDbContext>();
|
||||
var storeDbContext = scopedServices.GetRequiredService<EventStoreDbContext>();
|
||||
var domainStoreDbContext = scopedServices.GetRequiredService<DomainNotificationStoreDbContext>();
|
||||
var accessor = DatabaseAccessor.GetOrCreateAsync(_instanceDatabaseName);
|
||||
var applicationDbContext = accessor.CreateDatabase(scopedServices);
|
||||
|
||||
applicationDbContext.EnsureMigrationsApplied();
|
||||
|
||||
var creator2 = (RelationalDatabaseCreator)storeDbContext.Database
|
||||
.GetService<IRelationalDatabaseCreator>();
|
||||
creator2.CreateTables();
|
||||
|
||||
var creator3 = (RelationalDatabaseCreator)domainStoreDbContext
|
||||
.Database.GetService<IRelationalDatabaseCreator>();
|
||||
creator3.CreateTables();
|
||||
|
||||
_addCustomSeedDataHandler?.Invoke(applicationDbContext);
|
||||
_registerCustomServicesHandler?.Invoke(services, sp, scopedServices);
|
||||
});
|
||||
}
|
||||
|
||||
public async Task RespawnDatabaseAsync()
|
||||
{
|
||||
var accessor = DatabaseAccessor.GetOrCreateAsync(_instanceDatabaseName);
|
||||
await accessor.RespawnDatabaseAsync();
|
||||
}
|
||||
|
||||
public override async ValueTask DisposeAsync()
|
||||
{
|
||||
var accessor = DatabaseAccessor.GetOrCreateAsync(_instanceDatabaseName);
|
||||
await accessor.DisposeAsync();
|
||||
}
|
||||
}
|
@ -0,0 +1,125 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading.Tasks;
|
||||
using CleanArchitecture.Infrastructure.Database;
|
||||
using CleanArchitecture.Infrastructure.Extensions;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Respawn;
|
||||
using Testcontainers.MsSql;
|
||||
|
||||
namespace CleanArchitecture.IntegrationTests.Infrastructure;
|
||||
|
||||
public sealed class DatabaseAccessor
|
||||
{
|
||||
private static readonly ConcurrentDictionary<string, DatabaseAccessor> s_accessors = new();
|
||||
|
||||
private readonly string _instanceDatabaseName;
|
||||
private bool _databaseCreated = false;
|
||||
private readonly object _databaseCreationLock = new();
|
||||
|
||||
private const string _dbPassword = "12345678##as";
|
||||
private static readonly MsSqlContainer s_dbContainer = new MsSqlBuilder()
|
||||
.WithPassword(_dbPassword)
|
||||
.WithPortBinding(1433)
|
||||
.Build();
|
||||
|
||||
public DatabaseAccessor(string instanceName)
|
||||
{
|
||||
_instanceDatabaseName = instanceName;
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
await s_dbContainer.StartAsync();
|
||||
}
|
||||
|
||||
public ApplicationDbContext CreateDatabase(IServiceProvider scopedServices)
|
||||
{
|
||||
var applicationDbContext = scopedServices.GetRequiredService<ApplicationDbContext>();
|
||||
|
||||
lock (_databaseCreationLock)
|
||||
{
|
||||
if (_databaseCreated)
|
||||
{
|
||||
return applicationDbContext;
|
||||
}
|
||||
|
||||
applicationDbContext.EnsureMigrationsApplied();
|
||||
|
||||
var eventsContext = scopedServices.GetRequiredService<EventStoreDbContext>();
|
||||
eventsContext.EnsureMigrationsApplied();
|
||||
|
||||
var notificationsContext = scopedServices.GetRequiredService<DomainNotificationStoreDbContext>();
|
||||
notificationsContext.EnsureMigrationsApplied();
|
||||
}
|
||||
|
||||
_databaseCreated = true;
|
||||
|
||||
return applicationDbContext;
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
var dropScript = $@"
|
||||
USE MASTER;
|
||||
|
||||
ALTER DATABASE [{_instanceDatabaseName}]
|
||||
SET multi_user WITH ROLLBACK IMMEDIATE;
|
||||
|
||||
ALTER DATABASE [{_instanceDatabaseName}]
|
||||
SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
|
||||
|
||||
DROP DATABASE [{_instanceDatabaseName}];";
|
||||
|
||||
await using (var con = new SqlConnection(GetConnectionString()))
|
||||
{
|
||||
await con.OpenAsync();
|
||||
|
||||
var cmd = new SqlCommand(dropScript, con);
|
||||
await cmd.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
await s_dbContainer.DisposeAsync();
|
||||
}
|
||||
|
||||
public async Task RespawnDatabaseAsync()
|
||||
{
|
||||
var connectionString = GetConnectionString();
|
||||
|
||||
var respawn = await Respawner.CreateAsync(
|
||||
connectionString,
|
||||
new RespawnerOptions
|
||||
{
|
||||
TablesToIgnore = ["__EFMigrationsHistory"]
|
||||
});
|
||||
|
||||
await respawn.ResetAsync(connectionString);
|
||||
}
|
||||
|
||||
public string GetConnectionString()
|
||||
{
|
||||
var conBuilder = new SqlConnectionStringBuilder()
|
||||
{
|
||||
DataSource = s_dbContainer.Hostname,
|
||||
InitialCatalog = _instanceDatabaseName,
|
||||
IntegratedSecurity = false,
|
||||
Password = _dbPassword,
|
||||
UserID = "sa",
|
||||
TrustServerCertificate = true
|
||||
};
|
||||
|
||||
return conBuilder.ToString();
|
||||
}
|
||||
|
||||
public static DatabaseAccessor GetOrCreateAsync(string instanceName)
|
||||
{
|
||||
if (!s_accessors.TryGetValue(instanceName, out _))
|
||||
{
|
||||
var accessor = new DatabaseAccessor(instanceName);
|
||||
s_accessors.TryAdd(instanceName, accessor);
|
||||
}
|
||||
|
||||
return s_accessors[instanceName];
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
using CleanArchitecture.IntegrationTests.Fixtures;
|
||||
using Xunit;
|
||||
|
||||
namespace CleanArchitecture.IntegrationTests;
|
||||
|
||||
[CollectionDefinition("IntegrationTests", DisableParallelization = true)]
|
||||
public sealed class IntegrationTestsCollection :
|
||||
ICollectionFixture<DatabaseFixture>
|
||||
{
|
||||
}
|
@ -5,9 +5,12 @@ using CleanArchitecture.IntegrationTests.Fixtures.gRPC;
|
||||
using CleanArchitecture.Proto.Tenants;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
using Xunit.Priority;
|
||||
|
||||
namespace CleanArchitecture.IntegrationTests.gRPC;
|
||||
|
||||
[Collection("IntegrationTests")]
|
||||
[TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)]
|
||||
public sealed class GetTenantsByIdsTests : IClassFixture<GetTenantsByIdsTestFixture>
|
||||
{
|
||||
private readonly GetTenantsByIdsTestFixture _fixture;
|
||||
|
@ -4,9 +4,12 @@ using CleanArchitecture.IntegrationTests.Fixtures.gRPC;
|
||||
using CleanArchitecture.Proto.Users;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
using Xunit.Priority;
|
||||
|
||||
namespace CleanArchitecture.IntegrationTests.gRPC;
|
||||
|
||||
[Collection("IntegrationTests")]
|
||||
[TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)]
|
||||
public sealed class GetUsersByIdsTests : IClassFixture<GetUsersByIdsTestFixture>
|
||||
{
|
||||
private readonly GetUsersByIdsTestFixture _fixture;
|
||||
|
Loading…
Reference in New Issue
Block a user