0
0
mirror of https://github.com/alex289/CleanArchitecture.git synced 2025-07-12 00:25:05 +00:00

feat: Use NUnit for Integration tests

This commit is contained in:
alex289 2024-08-03 14:22:17 +02:00
parent 4bea7d66a8
commit 336bfc1d11
No known key found for this signature in database
GPG Key ID: 573F77CD2D87F863
23 changed files with 179 additions and 542 deletions

View File

@ -10,6 +10,6 @@
"Host": "localhost", "Host": "localhost",
"Username": "guest", "Username": "guest",
"Password": "guest", "Password": "guest",
"Enabled": "False" "Enabled": "True"
} }
} }

View File

@ -5,6 +5,7 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -5,6 +5,7 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -5,6 +5,7 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -5,6 +5,7 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@ -12,17 +13,17 @@
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.7" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="8.0.7" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="8.0.7" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="NUnit" Version="4.1.0" />
<PackageReference Include="NUnit.Analyzers" Version="4.2.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
<PackageReference Include="Respawn" Version="6.2.1" /> <PackageReference Include="Respawn" Version="6.2.1" />
<PackageReference Include="Testcontainers" Version="3.9.0" /> <PackageReference Include="Testcontainers" Version="3.9.0" />
<PackageReference Include="Testcontainers.MsSql" Version="3.9.0" /> <PackageReference Include="Testcontainers.MsSql" Version="3.9.0" />
<PackageReference Include="Testcontainers.RabbitMq" Version="3.9.0" /> <PackageReference Include="Testcontainers.RabbitMq" Version="3.9.0" />
<PackageReference Include="Testcontainers.Redis" Version="3.9.0" /> <PackageReference Include="Testcontainers.Redis" Version="3.9.0" />
<PackageReference Include="xunit" Version="2.9.0" />
<PackageReference Include="Xunit.Priority" Version="1.1.6" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.2"> <PackageReference Include="coverlet.collector" Version="6.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
@ -34,5 +35,7 @@
<ProjectReference Include="..\CleanArchitecture.Infrastructure\CleanArchitecture.Infrastructure.csproj" /> <ProjectReference Include="..\CleanArchitecture.Infrastructure\CleanArchitecture.Infrastructure.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Using Include="NUnit.Framework" />
</ItemGroup>
</Project> </Project>

View File

@ -0,0 +1,8 @@
namespace CleanArchitecture.IntegrationTests.Constants;
public static class Configuration
{
public const int RedisPort = 6379;
public const int MsSqlPort = 1433;
public const int RabbitMqPort = 5673;
}

View File

@ -7,24 +7,17 @@ using CleanArchitecture.Application.ViewModels.Tenants;
using CleanArchitecture.IntegrationTests.Extensions; using CleanArchitecture.IntegrationTests.Extensions;
using CleanArchitecture.IntegrationTests.Fixtures; using CleanArchitecture.IntegrationTests.Fixtures;
using FluentAssertions; using FluentAssertions;
using Xunit;
using Xunit.Priority;
namespace CleanArchitecture.IntegrationTests.Controller; namespace CleanArchitecture.IntegrationTests.Controller;
[Collection("IntegrationTests")] public sealed class TenantControllerTests
[TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)]
public sealed class TenantControllerTests : IClassFixture<TenantTestFixture>
{ {
private readonly TenantTestFixture _fixture; private readonly TenantTestFixture _fixture = new();
public TenantControllerTests(TenantTestFixture fixture) [OneTimeSetUp]
{ public async Task Setup() => await _fixture.SeedTestData();
_fixture = fixture;
}
[Fact] [Test, Order(0)]
[Priority(0)]
public async Task Should_Get_Tenant_By_Id() public async Task Should_Get_Tenant_By_Id()
{ {
var response = await _fixture.ServerClient.GetAsync($"/api/v1/Tenant/{_fixture.CreatedTenantId}"); var response = await _fixture.ServerClient.GetAsync($"/api/v1/Tenant/{_fixture.CreatedTenantId}");
@ -41,8 +34,7 @@ public sealed class TenantControllerTests : IClassFixture<TenantTestFixture>
message.Data.Users.Count().Should().Be(1); message.Data.Users.Count().Should().Be(1);
} }
[Fact] [Test, Order(1)]
[Priority(5)]
public async Task Should_Get_All_Tenants() public async Task Should_Get_All_Tenants()
{ {
var response = await _fixture.ServerClient.GetAsync( var response = await _fixture.ServerClient.GetAsync(
@ -63,8 +55,7 @@ public sealed class TenantControllerTests : IClassFixture<TenantTestFixture>
.Users.Count().Should().Be(1); .Users.Count().Should().Be(1);
} }
[Fact] [Test, Order(2)]
[Priority(10)]
public async Task Should_Create_Tenant() public async Task Should_Create_Tenant()
{ {
var request = new CreateTenantViewModel("Test Tenant 2"); var request = new CreateTenantViewModel("Test Tenant 2");
@ -89,8 +80,7 @@ public sealed class TenantControllerTests : IClassFixture<TenantTestFixture>
tenantMessage.Data.Name.Should().Be(request.Name); tenantMessage.Data.Name.Should().Be(request.Name);
} }
[Fact] [Test, Order(3)]
[Priority(15)]
public async Task Should_Update_Tenant() public async Task Should_Update_Tenant()
{ {
var request = new UpdateTenantViewModel(_fixture.CreatedTenantId, "Test Tenant 3"); var request = new UpdateTenantViewModel(_fixture.CreatedTenantId, "Test Tenant 3");
@ -117,8 +107,7 @@ public sealed class TenantControllerTests : IClassFixture<TenantTestFixture>
tenantMessage.Data.Name.Should().Be(request.Name); tenantMessage.Data.Name.Should().Be(request.Name);
} }
[Fact] [Test, Order(4)]
[Priority(20)]
public async Task Should_Delete_Tenant() public async Task Should_Delete_Tenant()
{ {
var response = await _fixture.ServerClient.DeleteAsync($"/api/v1/Tenant/{_fixture.CreatedTenantId}"); var response = await _fixture.ServerClient.DeleteAsync($"/api/v1/Tenant/{_fixture.CreatedTenantId}");

View File

@ -10,24 +10,17 @@ using CleanArchitecture.IntegrationTests.Extensions;
using CleanArchitecture.IntegrationTests.Fixtures; using CleanArchitecture.IntegrationTests.Fixtures;
using CleanArchitecture.IntegrationTests.Infrastructure.Auth; using CleanArchitecture.IntegrationTests.Infrastructure.Auth;
using FluentAssertions; using FluentAssertions;
using Xunit;
using Xunit.Priority;
namespace CleanArchitecture.IntegrationTests.Controller; namespace CleanArchitecture.IntegrationTests.Controller;
[Collection("IntegrationTests")] public sealed class UserControllerTests
[TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)]
public sealed class UserControllerTests : IClassFixture<UserTestFixture>
{ {
private readonly UserTestFixture _fixture; private readonly UserTestFixture _fixture = new();
public UserControllerTests(UserTestFixture fixture) [OneTimeSetUp]
{ public async Task Setup() => await GlobalSetupFixture.RespawnDatabaseAsync();
_fixture = fixture;
}
[Fact] [Test, Order(0)]
[Priority(0)]
public async Task Should_Get_All_User() public async Task Should_Get_All_User()
{ {
var response = await _fixture.ServerClient.GetAsync("/api/v1/user"); var response = await _fixture.ServerClient.GetAsync("/api/v1/user");
@ -50,8 +43,7 @@ public sealed class UserControllerTests : IClassFixture<UserTestFixture>
currentUser.LastName.Should().Be(TestAuthenticationOptions.LastName); currentUser.LastName.Should().Be(TestAuthenticationOptions.LastName);
} }
[Fact] [Test, Order(1)]
[Priority(5)]
public async Task Should_Get_User_By_Id() public async Task Should_Get_User_By_Id()
{ {
var response = await _fixture.ServerClient.GetAsync("/api/v1/user/" + TestAuthenticationOptions.TestUserId); var response = await _fixture.ServerClient.GetAsync("/api/v1/user/" + TestAuthenticationOptions.TestUserId);
@ -70,8 +62,7 @@ public sealed class UserControllerTests : IClassFixture<UserTestFixture>
content.LastName.Should().Be(TestAuthenticationOptions.LastName); content.LastName.Should().Be(TestAuthenticationOptions.LastName);
} }
[Fact] [Test, Order(2)]
[Priority(10)]
public async Task Should_Create_User() public async Task Should_Create_User()
{ {
var user = new CreateUserViewModel( var user = new CreateUserViewModel(
@ -89,8 +80,7 @@ public sealed class UserControllerTests : IClassFixture<UserTestFixture>
message?.Data.Should().NotBeEmpty(); message?.Data.Should().NotBeEmpty();
} }
[Fact] [Test, Order(3)]
[Priority(15)]
public async Task Should_Login_User() public async Task Should_Login_User()
{ {
var user = new LoginUserViewModel( var user = new LoginUserViewModel(
@ -105,8 +95,7 @@ public sealed class UserControllerTests : IClassFixture<UserTestFixture>
message?.Data.Should().NotBeEmpty(); message?.Data.Should().NotBeEmpty();
} }
[Fact] [Test, Order(4)]
[Priority(20)]
public async Task Should_Get_The_Current_Active_Users() public async Task Should_Get_The_Current_Active_Users()
{ {
var response = await _fixture.ServerClient.GetAsync("/api/v1/user/me"); var response = await _fixture.ServerClient.GetAsync("/api/v1/user/me");
@ -125,8 +114,7 @@ public sealed class UserControllerTests : IClassFixture<UserTestFixture>
content.LastName.Should().Be(TestAuthenticationOptions.LastName); content.LastName.Should().Be(TestAuthenticationOptions.LastName);
} }
[Fact] [Test, Order(5)]
[Priority(25)]
public async Task Should_Update_User() public async Task Should_Update_User()
{ {
var user = new UpdateUserViewModel( var user = new UpdateUserViewModel(
@ -167,8 +155,7 @@ public sealed class UserControllerTests : IClassFixture<UserTestFixture>
userContent.Role.Should().Be(user.Role); userContent.Role.Should().Be(user.Role);
} }
[Fact] [Test, Order(6)]
[Priority(30)]
public async Task Should_Change_User_Password() public async Task Should_Change_User_Password()
{ {
var user = new ChangePasswordViewModel( var user = new ChangePasswordViewModel(
@ -201,8 +188,7 @@ public sealed class UserControllerTests : IClassFixture<UserTestFixture>
loginMessage?.Data.Should().NotBeEmpty(); loginMessage?.Data.Should().NotBeEmpty();
} }
[Fact] [Test, Order(7)]
[Priority(35)]
public async Task Should_Delete_User() public async Task Should_Delete_User()
{ {
var response = await _fixture.ServerClient.DeleteAsync("/api/v1/user/" + TestAuthenticationOptions.TestUserId); var response = await _fixture.ServerClient.DeleteAsync("/api/v1/user/" + TestAuthenticationOptions.TestUserId);

View File

@ -1,35 +0,0 @@
using System;
using System.Threading.Tasks;
using CleanArchitecture.IntegrationTests.Infrastructure;
using Xunit;
namespace CleanArchitecture.IntegrationTests.Fixtures;
public sealed class AccessorFixture : IAsyncLifetime
{
public static string TestRunDbName { get; } = $"CleanArchitecture-Integration-{Guid.NewGuid()}";
public async Task DisposeAsync()
{
var db = DatabaseAccessor.GetOrCreateAsync(TestRunDbName);
await db.DisposeAsync();
var redis = RedisAccessor.GetOrCreateAsync();
await redis.DisposeAsync();
var rabbit = RabbitmqAccessor.GetOrCreateAsync();
await rabbit.DisposeAsync();
}
public async Task InitializeAsync()
{
var db = DatabaseAccessor.GetOrCreateAsync(TestRunDbName);
await db.InitializeAsync();
var redis = RedisAccessor.GetOrCreateAsync();
await redis.InitializeAsync();
var rabbit = RabbitmqAccessor.GetOrCreateAsync();
await rabbit.InitializeAsync();
}
}

View File

@ -1,7 +1,9 @@
using System; using System;
using System.Threading.Tasks;
using CleanArchitecture.Domain.Entities; using CleanArchitecture.Domain.Entities;
using CleanArchitecture.Domain.Enums; using CleanArchitecture.Domain.Enums;
using CleanArchitecture.Infrastructure.Database; using CleanArchitecture.Infrastructure.Database;
using Microsoft.Extensions.DependencyInjection;
namespace CleanArchitecture.IntegrationTests.Fixtures; namespace CleanArchitecture.IntegrationTests.Fixtures;
@ -9,9 +11,11 @@ public sealed class TenantTestFixture : TestFixtureBase
{ {
public Guid CreatedTenantId { get; } = Guid.NewGuid(); public Guid CreatedTenantId { get; } = Guid.NewGuid();
protected override void SeedTestData(ApplicationDbContext context) public async Task SeedTestData()
{ {
base.SeedTestData(context); await GlobalSetupFixture.RespawnDatabaseAsync();
using var context = Factory.Services.GetRequiredService<ApplicationDbContext>();
context.Tenants.Add(new Tenant( context.Tenants.Add(new Tenant(
CreatedTenantId, CreatedTenantId,
@ -26,6 +30,6 @@ public sealed class TenantTestFixture : TestFixtureBase
"Test User", "Test User",
UserRole.User)); UserRole.User));
context.SaveChanges(); await context.SaveChangesAsync();
} }
} }

View File

@ -1,18 +1,11 @@
using System; using System;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks;
using CleanArchitecture.Domain.Constants;
using CleanArchitecture.Domain.Entities;
using CleanArchitecture.Domain.Enums;
using CleanArchitecture.Infrastructure.Database;
using CleanArchitecture.IntegrationTests.Infrastructure; using CleanArchitecture.IntegrationTests.Infrastructure;
using CleanArchitecture.IntegrationTests.Infrastructure.Auth;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Xunit;
namespace CleanArchitecture.IntegrationTests.Fixtures; namespace CleanArchitecture.IntegrationTests.Fixtures;
public class TestFixtureBase : IAsyncLifetime public class TestFixtureBase
{ {
public HttpClient ServerClient { get; } public HttpClient ServerClient { get; }
protected CleanArchitectureWebApplicationFactory Factory { get; } protected CleanArchitectureWebApplicationFactory Factory { get; }
@ -21,64 +14,16 @@ public class TestFixtureBase : IAsyncLifetime
{ {
Factory = new CleanArchitectureWebApplicationFactory( Factory = new CleanArchitectureWebApplicationFactory(
RegisterCustomServicesHandler, RegisterCustomServicesHandler,
useTestAuthentication, useTestAuthentication);
AccessorFixture.TestRunDbName);
ServerClient = Factory.CreateClient(); ServerClient = Factory.CreateClient();
ServerClient.Timeout = TimeSpan.FromMinutes(5); ServerClient.Timeout = TimeSpan.FromMinutes(5);
} }
protected virtual void SeedTestData(ApplicationDbContext context)
{
}
private async Task PrepareDatabaseAsync()
{
await Factory.RespawnDatabaseAsync();
using var scope = Factory.Services.CreateScope();
await using var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
dbContext.Tenants.Add(new Tenant(
Ids.Seed.TenantId,
"Admin Tenant"));
dbContext.Users.Add(new User(
Ids.Seed.UserId,
Ids.Seed.TenantId,
"admin@email.com",
"Admin",
"User",
"$2a$12$Blal/uiFIJdYsCLTMUik/egLbfg3XhbnxBC6Sb5IKz2ZYhiU/MzL2",
UserRole.Admin));
dbContext.Users.Add(new User(
TestAuthenticationOptions.TestUserId,
Ids.Seed.TenantId,
TestAuthenticationOptions.Email,
TestAuthenticationOptions.FirstName,
TestAuthenticationOptions.LastName,
TestAuthenticationOptions.Password,
UserRole.Admin));
SeedTestData(dbContext);
await dbContext.SaveChangesAsync();
}
protected virtual void RegisterCustomServicesHandler( protected virtual void RegisterCustomServicesHandler(
IServiceCollection services, IServiceCollection services,
ServiceProvider serviceProvider, ServiceProvider serviceProvider,
IServiceProvider scopedServices) IServiceProvider scopedServices)
{ {
} }
public async Task InitializeAsync()
{
await PrepareDatabaseAsync();
}
public Task DisposeAsync()
{
return Task.CompletedTask;
}
} }

View File

@ -1,7 +1,9 @@
using System; using System;
using System.Threading.Tasks;
using CleanArchitecture.Domain.Entities; using CleanArchitecture.Domain.Entities;
using CleanArchitecture.Infrastructure.Database; using CleanArchitecture.Infrastructure.Database;
using Grpc.Net.Client; using Grpc.Net.Client;
using Microsoft.Extensions.DependencyInjection;
namespace CleanArchitecture.IntegrationTests.Fixtures.gRPC; namespace CleanArchitecture.IntegrationTests.Fixtures.gRPC;
@ -18,14 +20,16 @@ public sealed class GetTenantsByIdsTestFixture : TestFixtureBase
}); });
} }
protected override void SeedTestData(ApplicationDbContext context) public async Task SeedTestData()
{ {
base.SeedTestData(context); await GlobalSetupFixture.RespawnDatabaseAsync();
using var context = Factory.Services.GetRequiredService<ApplicationDbContext>();
var tenant = CreateTenant(); var tenant = CreateTenant();
context.Tenants.Add(tenant); context.Tenants.Add(tenant);
context.SaveChanges(); await context.SaveChangesAsync();
} }
public Tenant CreateTenant() public Tenant CreateTenant()

View File

@ -1,9 +1,11 @@
using System; using System;
using System.Threading.Tasks;
using CleanArchitecture.Domain.Constants; using CleanArchitecture.Domain.Constants;
using CleanArchitecture.Domain.Entities; using CleanArchitecture.Domain.Entities;
using CleanArchitecture.Domain.Enums; using CleanArchitecture.Domain.Enums;
using CleanArchitecture.Infrastructure.Database; using CleanArchitecture.Infrastructure.Database;
using Grpc.Net.Client; using Grpc.Net.Client;
using Microsoft.Extensions.DependencyInjection;
namespace CleanArchitecture.IntegrationTests.Fixtures.gRPC; namespace CleanArchitecture.IntegrationTests.Fixtures.gRPC;
@ -20,14 +22,16 @@ public sealed class GetUsersByIdsTestFixture : TestFixtureBase
}); });
} }
protected override void SeedTestData(ApplicationDbContext context) public async Task SeedTestData()
{ {
base.SeedTestData(context); await GlobalSetupFixture.RespawnDatabaseAsync();
using var context = Factory.Services.GetRequiredService<ApplicationDbContext>();
var user = CreateUser(); var user = CreateUser();
context.Users.Add(user); context.Users.Add(user);
context.SaveChanges(); await context.SaveChangesAsync();
} }
public User CreateUser() public User CreateUser()

View File

@ -0,0 +1,75 @@
using System;
using System.Threading.Tasks;
using CleanArchitecture.IntegrationTests.Constants;
using Respawn;
using Testcontainers.MsSql;
using Testcontainers.RabbitMq;
using Testcontainers.Redis;
namespace CleanArchitecture.IntegrationTests;
[SetUpFixture]
internal class GlobalSetupFixture
{
private static Respawner? s_respawner;
public static MsSqlContainer DatabaseContainer { get; } = new MsSqlBuilder()
.WithPortBinding(Configuration.MsSqlPort, assignRandomHostPort: true)
.Build();
public static RedisContainer RedisContainer { get; } = new RedisBuilder()
.WithPortBinding(Configuration.RedisPort, assignRandomHostPort: true)
.Build();
public static RabbitMqContainer RabbitContainer { get; } = new RabbitMqBuilder()
.WithPortBinding(Configuration.RabbitMqPort, assignRandomHostPort: true)
.Build();
public static string DatabaseConnectionString { get; private set; } = string.Empty;
[OneTimeSetUp]
public async Task SetUp()
{
await DatabaseContainer.StartAsync();
await RedisContainer.StartAsync();
await RabbitContainer.StartAsync();
DatabaseConnectionString = DatabaseContainer
.GetConnectionString()
.Replace("Database=master", $"Database=clean-architecture-{Guid.NewGuid()}");
}
[OneTimeTearDown]
public async Task TearDown()
{
await DatabaseContainer.DisposeAsync();
await RedisContainer.DisposeAsync();
await RabbitContainer.DisposeAsync();
}
public static async Task RespawnDatabaseAsync()
{
if (s_respawner is null)
{
try
{
s_respawner = await Respawner.CreateAsync(
DatabaseConnectionString,
new RespawnerOptions
{
TablesToIgnore = ["__EFMigrationsHistory"]
});
}
catch (Exception ex)
{
// Creation of the respawner can fail if the database has not been created yet
TestContext.WriteLine($"Failed to create respawner: {ex.Message}");
}
}
if (s_respawner is not null)
{
await s_respawner.ResetAsync(DatabaseConnectionString);
}
}
}

View File

@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using CleanArchitecture.IntegrationTests.Constants;
using CleanArchitecture.Infrastructure.Database;
using CleanArchitecture.IntegrationTests.Infrastructure.Auth; using CleanArchitecture.IntegrationTests.Infrastructure.Auth;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.Mvc.Testing;
@ -12,26 +11,20 @@ namespace CleanArchitecture.IntegrationTests.Infrastructure;
public sealed class CleanArchitectureWebApplicationFactory : WebApplicationFactory<Program> public sealed class CleanArchitectureWebApplicationFactory : WebApplicationFactory<Program>
{ {
public delegate void AddCustomSeedDataHandler(ApplicationDbContext context);
public delegate void RegisterCustomServicesHandler( public delegate void RegisterCustomServicesHandler(
IServiceCollection services, IServiceCollection services,
ServiceProvider serviceProvider, ServiceProvider serviceProvider,
IServiceProvider scopedServices); IServiceProvider scopedServices);
private readonly string _instanceDatabaseName;
private readonly bool _addTestAuthentication; private readonly bool _addTestAuthentication;
private readonly RegisterCustomServicesHandler? _registerCustomServicesHandler; private readonly RegisterCustomServicesHandler? _registerCustomServicesHandler;
public CleanArchitectureWebApplicationFactory( public CleanArchitectureWebApplicationFactory(
RegisterCustomServicesHandler? registerCustomServicesHandler, RegisterCustomServicesHandler? registerCustomServicesHandler,
bool addTestAuthentication, bool addTestAuthentication)
string instanceDatabaseName)
{ {
_registerCustomServicesHandler = registerCustomServicesHandler; _registerCustomServicesHandler = registerCustomServicesHandler;
_addTestAuthentication = addTestAuthentication; _addTestAuthentication = addTestAuthentication;
_instanceDatabaseName = instanceDatabaseName;
} }
protected override void ConfigureWebHost(IWebHostBuilder builder) protected override void ConfigureWebHost(IWebHostBuilder builder)
@ -40,31 +33,18 @@ public sealed class CleanArchitectureWebApplicationFactory : WebApplicationFacto
base.ConfigureWebHost(builder); base.ConfigureWebHost(builder);
var configuration = new ConfigurationBuilder() builder.ConfigureAppConfiguration(configuration =>
.Build();
builder.ConfigureAppConfiguration(configurationBuilder =>
{ {
configurationBuilder.AddEnvironmentVariables(); var redisPort = GlobalSetupFixture.RedisContainer.GetMappedPublicPort(Configuration.RedisPort);
var dbAccessor = DatabaseAccessor.GetOrCreateAsync(_instanceDatabaseName); configuration.AddInMemoryCollection([
var redisAccessor = RedisAccessor.GetOrCreateAsync();
var rabbitAccessor = RabbitmqAccessor.GetOrCreateAsync();
// Overwrite default connection strings
configurationBuilder.AddInMemoryCollection([
new KeyValuePair<string, string?>( new KeyValuePair<string, string?>(
"ConnectionStrings:DefaultConnection", "ConnectionStrings:DefaultConnection",
dbAccessor.GetConnectionString()), GlobalSetupFixture.DatabaseConnectionString),
new KeyValuePair<string, string?>( new KeyValuePair<string, string?>(
"RedisHostName", "RedisStackExchange:RedisConfigString",
redisAccessor.GetConnectionString()), $"localhost:{redisPort},abortConnect=true")
new KeyValuePair<string, string?>(
"RabbitMQ:Host",
rabbitAccessor.GetConnectionString())
]); ]);
configuration = configurationBuilder.Build();
}); });
builder.ConfigureServices(services => builder.ConfigureServices(services =>
@ -82,38 +62,7 @@ public sealed class CleanArchitectureWebApplicationFactory : WebApplicationFacto
using var scope = sp.CreateScope(); using var scope = sp.CreateScope();
var scopedServices = scope.ServiceProvider; var scopedServices = scope.ServiceProvider;
var dbAccessor = DatabaseAccessor.GetOrCreateAsync(_instanceDatabaseName);
dbAccessor.CreateDatabase(scopedServices);
var redisAccessor = RedisAccessor.GetOrCreateAsync();
redisAccessor.RegisterRedis(services, configuration);
var rabbitAccessor = RabbitmqAccessor.GetOrCreateAsync();
rabbitAccessor.RegisterRabbitmq(services, configuration);
_registerCustomServicesHandler?.Invoke(services, sp, scopedServices); _registerCustomServicesHandler?.Invoke(services, sp, scopedServices);
}); });
} }
public async Task RespawnDatabaseAsync()
{
var dbAccessor = DatabaseAccessor.GetOrCreateAsync(_instanceDatabaseName);
await dbAccessor.RespawnDatabaseAsync();
var redisAccessor = RedisAccessor.GetOrCreateAsync();
redisAccessor.ResetRedis();
}
public override async ValueTask DisposeAsync()
{
var dbAccessor = DatabaseAccessor.GetOrCreateAsync(_instanceDatabaseName);
await dbAccessor.DisposeAsync();
var redisAccessor = RedisAccessor.GetOrCreateAsync();
await redisAccessor.DisposeAsync();
var rabbitAccessor = RabbitmqAccessor.GetOrCreateAsync();
await rabbitAccessor.DisposeAsync();
}
} }

View File

@ -1,126 +0,0 @@
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 = "234#AD224fD#ss";
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()
{
// Reset the database to its original state
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];
}
}

View File

@ -1,64 +0,0 @@
using System.Collections.Concurrent;
using System.Linq;
using System.Threading.Tasks;
using CleanArchitecture.Domain.Rabbitmq;
using CleanArchitecture.Domain.Rabbitmq.Extensions;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Testcontainers.RabbitMq;
using RabbitMqConfiguration = CleanArchitecture.Domain.Rabbitmq.RabbitMqConfiguration;
namespace CleanArchitecture.IntegrationTests.Infrastructure;
public sealed class RabbitmqAccessor
{
private static readonly ConcurrentDictionary<string, RabbitmqAccessor> s_accessors = new();
private static readonly RabbitMqContainer s_rabbitContainer = new RabbitMqBuilder()
.WithPortBinding(5672)
.Build();
public async Task InitializeAsync()
{
await s_rabbitContainer.StartAsync();
}
public async ValueTask DisposeAsync()
{
await s_rabbitContainer.DisposeAsync();
}
public string GetConnectionString()
{
return s_rabbitContainer.GetConnectionString();
}
public void RegisterRabbitmq(IServiceCollection serviceCollection, IConfiguration configuration)
{
var rabbitService = serviceCollection.FirstOrDefault(x =>
x.ServiceType == typeof(RabbitMqHandler));
if (rabbitService != null)
{
serviceCollection.Remove(rabbitService);
}
var rabbitConfig = serviceCollection.FirstOrDefault(x =>
x.ServiceType == typeof(RabbitMqConfiguration));
if (rabbitConfig != null)
{
serviceCollection.Remove(rabbitConfig);
}
serviceCollection.AddRabbitMqHandler(
configuration,
"RabbitMQ");
}
public static RabbitmqAccessor GetOrCreateAsync()
{
return s_accessors.GetOrAdd("rabbimq", _ => new RabbitmqAccessor());
}
}

View File

@ -1,76 +0,0 @@
using System.Collections.Concurrent;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using StackExchange.Redis;
using Testcontainers.Redis;
namespace CleanArchitecture.IntegrationTests.Infrastructure;
public sealed class RedisAccessor
{
private static readonly ConcurrentDictionary<string, RedisAccessor> s_accessors = new();
private static readonly RedisContainer s_redisContainer = new RedisBuilder()
.WithPortBinding(6379)
.Build();
public async Task InitializeAsync()
{
await s_redisContainer.StartAsync();
}
public async ValueTask DisposeAsync()
{
await s_redisContainer.DisposeAsync();
}
public string GetConnectionString()
{
return s_redisContainer.GetConnectionString();
}
public void RegisterRedis(IServiceCollection serviceCollection, IConfiguration configuration)
{
var distributedCache = serviceCollection.FirstOrDefault(x =>
x.ServiceType == typeof(IDistributedCache));
if (distributedCache != null)
{
serviceCollection.Remove(distributedCache);
}
serviceCollection.AddStackExchangeRedisCache(options =>
{
options.Configuration = configuration["RedisHostName"];
options.InstanceName = "clean-architecture";
});
}
public void ResetRedis()
{
var redis = ConnectionMultiplexer.Connect(GetConnectionString());
var db = redis.GetDatabase();
var endpoints = redis.GetEndPoints();
foreach (var endpoint in endpoints)
{
var server = redis.GetServer(endpoint);
var keys = server.Keys();
foreach (var key in keys)
{
db.KeyDelete(key);
}
}
redis.Close();
}
public static RedisAccessor GetOrCreateAsync()
{
return s_accessors.GetOrAdd("redis", _ => new RedisAccessor());
}
}

View File

@ -1,10 +0,0 @@
using CleanArchitecture.IntegrationTests.Fixtures;
using Xunit;
namespace CleanArchitecture.IntegrationTests;
[CollectionDefinition("IntegrationTests", DisableParallelization = true)]
public sealed class IntegrationTestsCollection :
ICollectionFixture<AccessorFixture>
{
}

View File

@ -2,33 +2,30 @@ using System.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
using CleanArchitecture.IntegrationTests.Fixtures; using CleanArchitecture.IntegrationTests.Fixtures;
using FluentAssertions; using FluentAssertions;
using Xunit;
using Xunit.Priority;
namespace CleanArchitecture.IntegrationTests.UtilityTests; namespace CleanArchitecture.IntegrationTests.UtilityTests;
[Collection("IntegrationTests")] public sealed class AuthTests
[TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)]
public sealed class AuthTests : IClassFixture<AuthTestFixure>
{ {
private readonly AuthTestFixure _fixture; private readonly AuthTestFixure _fixture = new();
public AuthTests(AuthTestFixure fixture) [OneTimeSetUp]
{ public async Task Setup() => await GlobalSetupFixture.RespawnDatabaseAsync();
_fixture = fixture;
} [Datapoints]
public string[] values =
[
"/api/v1/user",
"/api/v1/user/me",
"/api/v1/user/d74b112a-ece0-443d-9b4f-85bc418822ca",
"/api/v1/tenant",
"/api/v1/tenant/d74b112a-ece0-443d-9b4f-85bc418822ca"
];
[Theory] [Theory]
[InlineData("/api/v1/user")] public async Task Should_Get_Unauthorized_If_Trying_To_Call_Endpoint_Without_Token(string url)
[InlineData("/api/v1/user/me")]
[InlineData("/api/v1/user/d74b112a-ece0-443d-9b4f-85bc418822ca")]
[InlineData("/api/v1/tenant")]
[InlineData("/api/v1/tenant/d74b112a-ece0-443d-9b4f-85bc418822ca")]
public async Task Should_Get_Unauthorized_If_Trying_To_Call_Endpoint_Without_Token(
string url)
{ {
var response = await _fixture.ServerClient.GetAsync(url); var response = await _fixture.ServerClient.GetAsync(url);
response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); response.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
} }
} }

View File

@ -4,24 +4,17 @@ using CleanArchitecture.IntegrationTests.Fixtures;
using FluentAssertions; using FluentAssertions;
using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Diagnostics.HealthChecks;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Xunit;
using Xunit.Priority;
namespace CleanArchitecture.IntegrationTests.UtilityTests; namespace CleanArchitecture.IntegrationTests.UtilityTests;
[Collection("IntegrationTests")] public sealed class HealthChecksTests
[TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)]
public sealed class HealthChecksTests : IClassFixture<AuthTestFixure>
{ {
private readonly AuthTestFixure _fixture; private readonly AuthTestFixure _fixture = new();
public HealthChecksTests(AuthTestFixure fixture) [OneTimeSetUp]
{ public async Task Setup() => await GlobalSetupFixture.RespawnDatabaseAsync();
_fixture = fixture;
}
[Fact] [Test, Order(0)]
[Priority(0)]
public async Task Should_Return_Healthy() public async Task Should_Return_Healthy()
{ {
var response = await _fixture.ServerClient.GetAsync("/healthz"); var response = await _fixture.ServerClient.GetAsync("/healthz");

View File

@ -4,23 +4,17 @@ using System.Threading.Tasks;
using CleanArchitecture.IntegrationTests.Fixtures.gRPC; using CleanArchitecture.IntegrationTests.Fixtures.gRPC;
using CleanArchitecture.Proto.Tenants; using CleanArchitecture.Proto.Tenants;
using FluentAssertions; using FluentAssertions;
using Xunit;
using Xunit.Priority;
namespace CleanArchitecture.IntegrationTests.gRPC; namespace CleanArchitecture.IntegrationTests.gRPC;
[Collection("IntegrationTests")] public sealed class GetTenantsByIdsTests
[TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)]
public sealed class GetTenantsByIdsTests : IClassFixture<GetTenantsByIdsTestFixture>
{ {
private readonly GetTenantsByIdsTestFixture _fixture; private readonly GetTenantsByIdsTestFixture _fixture = new();
public GetTenantsByIdsTests(GetTenantsByIdsTestFixture fixture) [OneTimeSetUp]
{ public async Task Setup() => await _fixture.SeedTestData();
_fixture = fixture;
}
[Fact] [Test]
public async Task Should_Get_Tenants_By_Ids() public async Task Should_Get_Tenants_By_Ids()
{ {
var client = new TenantsApi.TenantsApiClient(_fixture.GrpcChannel); var client = new TenantsApi.TenantsApiClient(_fixture.GrpcChannel);

View File

@ -3,23 +3,17 @@ using System.Threading.Tasks;
using CleanArchitecture.IntegrationTests.Fixtures.gRPC; using CleanArchitecture.IntegrationTests.Fixtures.gRPC;
using CleanArchitecture.Proto.Users; using CleanArchitecture.Proto.Users;
using FluentAssertions; using FluentAssertions;
using Xunit;
using Xunit.Priority;
namespace CleanArchitecture.IntegrationTests.gRPC; namespace CleanArchitecture.IntegrationTests.gRPC;
[Collection("IntegrationTests")] public sealed class GetUsersByIdsTests
[TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)]
public sealed class GetUsersByIdsTests : IClassFixture<GetUsersByIdsTestFixture>
{ {
private readonly GetUsersByIdsTestFixture _fixture; private readonly GetUsersByIdsTestFixture _fixture = new();
public GetUsersByIdsTests(GetUsersByIdsTestFixture fixture) [OneTimeSetUp]
{ public async Task Setup() => await _fixture.SeedTestData();
_fixture = fixture;
}
[Fact] [Test]
public async Task Should_Get_Users_By_Ids() public async Task Should_Get_Users_By_Ids()
{ {
var client = new UsersApi.UsersApiClient(_fixture.GrpcChannel); var client = new UsersApi.UsersApiClient(_fixture.GrpcChannel);