Reputation: 41
I’m trying to create integration tests in .NET 9 C# using xUnit and TestContainers. However, whenever I run the tests, a new database container is created for each test class. I want to initialize only one database container for all the test classes.
To solve this, I tried using Collections, so a single Collection initializes one database for all the tests declared in it. However, I’m receiving the following error:
The following constructor parameters did not have matching fixture data: IntegrationTestWebAppFactory factory
My code: DatabaseFixture
I use Respawn to reset the database after each test, and then I recreate test scenarios using seed data from SQL files for cases that require pre-existing data.
using System.Data.Common;
using Microsoft.Data.SqlClient;
using Respawn;
using Testcontainers.MsSql;
using Tests.Integration.Persistence.Models;
namespace Tests.Integration.Abstractions.Fixtures;
public class DatabaseFixture : IAsyncLifetime, IDisposable
{
private const string _testContainerPassword = "MyPassword";
private DbConnection _dbConnection = default!;
private Respawner _respawner = default!;
private Dictionary<string, Respawner> _isclaRespawners = default!;
private readonly MsSqlContainer _container = new MsSqlBuilder()
.WithImage("mcr.microsoft.com/mssql/server:2022-CU16-ubuntu-22.04")
.WithPassword(_testContainerPassword)
.Build();
public string ConnectionString => _container.GetConnectionString();
public async Task ResetDataBaseAsync()
{
await _respawner.ResetAsync(ConnectionString);
foreach (var i in _isclaRespawners)
{
await i.Value.ResetAsync(i.Key);
}
await SeedDatabase();
}
private async Task InitializeRespawner()
{
_dbConnection = new SqlConnection(ConnectionString);
await _dbConnection.OpenAsync();
var lstIscalaConnections = await SeedDatabase();
_isclaRespawners = new Dictionary<string, Respawner>();
_respawner = await Respawner.CreateAsync(_dbConnection, new RespawnerOptions
{
DbAdapter = DbAdapter.SqlServer,
SchemasToInclude = new[] {
"dbo",
},
WithReseed = true
});
foreach (var i in lstIscalaConnections)
{
if (!String.IsNullOrWhiteSpace(i.ServidorErp))
{
var iscalaConnectionString = ConnectionString
.Replace("MyDb", i.BaseErp);
if (!_isclaRespawners.TryGetValue(iscalaConnectionString, out var existingRespawner))
{
var respawner = await Respawner.CreateAsync(iscalaConnectionString, new RespawnerOptions
{
DbAdapter = DbAdapter.SqlServer,
SchemasToInclude = new[] {
"dbo",
},
WithReseed = true
});
_isclaRespawners.Add(iscalaConnectionString, respawner);
}
}
if (!String.IsNullOrWhiteSpace(i.BaseErpfd))
{
var iscalaFDConnectionString = ConnectionString
.Replace("MyDb", i.BaseErpfd);
if (!_isclaRespawners.TryGetValue(iscalaFDConnectionString, out var existingRespawner))
{
var respawnerFD = await Respawner.CreateAsync(iscalaFDConnectionString, new RespawnerOptions
{
DbAdapter = DbAdapter.SqlServer,
SchemasToInclude = new[] {
"dbo",
},
WithReseed = true
});
_isclaRespawners.Add(iscalaFDConnectionString, respawnerFD);
}
}
}
}
protected async Task<List<IscalaConnections>> SeedDatabase()
{
using (var command = _dbConnection.CreateCommand())
{
string baseSeedDir = "../../../Persistence/Seeds";
// Seed MyDb Empresas
string SeedSisautoScript = await File.ReadAllTextAsync($"{baseSeedDir}/SeedMyDb.sql");
command.CommandText = SeedSisautoScript;
await command.ExecuteScalarAsync();
// Initialize iScalaDb
if (!await IsIscalaDBInitialized(command))
{
string InitializeIscalaDbScript = await File.ReadAllTextAsync($"{baseSeedDir}/InitializeIscala.sql");
command.CommandText = InitializeIscalaDbScript;
await command.ExecuteScalarAsync();
}
// Seed iScalaDb
string SeedIscalaDbScript = await File.ReadAllTextAsync($"{baseSeedDir}/SeedIscala.sql");
command.CommandText = SeedIscalaDbScript;
await command.ExecuteScalarAsync();
var lstIscalaConnections = await GetCompanysInfo(command);
return lstIscalaConnections;
}
}
private static async Task<bool> IsIscalaDBInitialized(DbCommand command)
{
command.CommandText = "SELECT COUNT(*) FROM sys.databases";
var dbReader = await command.ExecuteReaderAsync();
if (await dbReader.ReadAsync())
{
var contagem = (int)dbReader[0];
await dbReader.CloseAsync();
if (contagem > 5)
{
return true;
}
}
return false;
}
private static async Task<List<IscalaConnections>> GetCompanysInfo(DbCommand dbSisautoCommand)
{
var lstConnections = new List<IscalaConnections>();
dbSisautoCommand.CommandText = @"SELECT
e.serverErp
,e.baseERP
,HasFD = CASE e.hasFD WHEN 1 THEN '1' ELSE '0' END
,e.serverErpFD
,e.baseERPFD
FROM
Company e
GROUP BY
e.serverErp
,e.baseERP
,e.temFD
,e.serverErpFD
,e.baseERPFD ";
var dbReader = await dbSisautoCommand.ExecuteReaderAsync();
while (await dbReader.ReadAsync())
{
var obj = new IscalaConnections
{
ServidorErp = dbReader["serverErp"].ToString()!,
BaseErp = dbReader["baseERP"].ToString()!,
withFatDir = dbReader["HasFD"].ToString() == "1",
ServidorErpfd = dbReader["serverErpFD"].ToString(),
BaseErpfd = dbReader["BaseErpfd"].ToString(),
};
lstConnections.Add(obj);
}
await dbReader.CloseAsync();
return lstConnections;
}
public async Task InitializeAsync()
{
await _container.StartAsync();
await InitializeRespawner();
}
public async Task DisposeAsync()
{
await _container.DisposeAsync();
await _dbConnection.DisposeAsync();
}
public void Dispose() {}
}
SharadTestDbCollection
using Tests.Integration.Abstractions.Fixtures;
namespace Integration.Tests.Abstractions;
[CollectionDefinition(nameof(SharadTestDbCollection))]
public class SharadTestDbCollection : ICollectionFixture<DatabaseFixture>;
IntegrationTestWebAppFactory
using Infra.Persistence.DBIscala;
using Infra.Persistence.DbSisauto;
using Infra.Services.Classes;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using Tests.Integration.Abstractions.Fixtures;
namespace Integration.Tests.Abstractions;
[Collection(nameof(SharadTestDbCollection))]
public class IntegrationTestWebAppFactory : WebApplicationFactory<Program>, IAsyncLifetime
{
private readonly DatabaseFixture _dbFixture;
public Func<Task> ResetDataBaseAsync;
private string _connectionString = null!;
public HttpClient HttpClient {get; private set;} = default!;
public IntegrationTestWebAppFactory(DatabaseFixture dbFixture)
{
_dbFixture = dbFixture;
ResetDataBaseAsync = dbFixture.ResetDataBaseAsync;
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
_connectionString = _dbFixture.ConnectionString;
_connectionString = _connectionString.Replace("master", "myDB");
builder.ConfigureTestServices(services => {
// Remove serviços
services.RemoveAll(typeof(DbContextOptions<MyDbContext>));
services.RemoveAll(typeof(DbContextOptions<DbIcalaContext>));
services.RemoveAll(typeof(IOptions<IscalaDbOptions>));
services.AddOptions();
services.AddDbContext<MyDbContext>(opt => {
opt.UseSqlServer(_connectionString);
});
services.AddDbContext<DbIcalaContext>();
var iscalaOptions = new IscalaDbOptions
{
ConnectionString = _connectionString.Replace("myDB", "{BaseERP}"),
};
services.AddSingleton<IOptions<IscalaDbOptions>>(_ => Options.Create(iscalaOptions));
});
}
public Task InitializeAsync()
{
HttpClient = CreateClient();
return Task.CompletedTask;
}
public Task DisposeAsync() => Task.CompletedTask;
}
SharadTestCollection
namespace Integration.Tests.Abstractions;
[CollectionDefinition(nameof(SharadTestCollection))]
public class SharadTestCollection : ICollectionFixture<IntegrationTestWebAppFactory>;
BaseIntegrationTest
using MediatR;
using Microsoft.Extensions.DependencyInjection;
namespace Integration.Tests.Abstractions;
[CollectionDefinition(nameof(SharadTestCollection))]
public class BaseIntegrationTest : IAsyncLifetime
{
protected readonly IServiceScope _scope;
private readonly Func<Task> _resetDatabase;
protected readonly ISender Sender;
protected readonly HttpClient _client;
protected BaseIntegrationTest(IntegrationTestWebAppFactory factory)
{
_scope = factory.Services.CreateScope();
Sender = _scope.ServiceProvider.GetRequiredService<ISender>();
_client = factory.HttpClient;
_resetDatabase = factory.ResetDataBaseAsync;
}
public virtual Task InitializeAsync() => Task.CompletedTask;
public virtual async Task DisposeAsync() {
await _resetDatabase();
_scope.Dispose();
_client.Dispose();
}
}
SupplierTests
using System;
using System.Net.Http.Json;
using Domain.EmpresaDomain;
using Domain.FornecedorDomain;
using Integration.Tests.Abstractions;
namespace Tests.Integration.Tests.Fornecedor;
public class SupplierTests : BaseIntegrationTest
{
public SupplierTests(IntegrationTestWebAppFactory factory) : base(factory)
{
}
[Fact]
public async Task ShuldReturn10()
{
// Arrange
_client.DefaultRequestHeaders
.Add("X-TenantId", "1");
// Act
var suppliers = await _client.GetFromJsonAsync<List<SupplierEntity>>("/api/supplier");
// Assert
Assert.NotNull(fornecedores);
Assert.IsType<List<SupplierEntity>>(fornecedores);
var qtdeSupplier = 10;
Assert.Equal(suppliers.Count, qtdeSupplier);
}
}
Previously, the database configuration was in IntegrationTestWebAppFactory, but as it was creating multiple databases, I tried to move it elsewhere. However, I couldn’t make it work with the above code.
Could someone help me identify what I’m doing wrong and how to fix it?
Upvotes: 1
Views: 67
Reputation: 156654
Rather than making your application factory a fixture, you'll want to create a separate fixture which is in charge of managing the application factory.
public sealed class HostAndDatabaseFixture : IAsyncLifetime
{
public DatabaseFixture DbFixture { get; }
public WebApplicationFactory<Program> WebApplicationFactory { get; }
public HttpClient Client { get; }
public HostAndDatabaseFixture()
{
DbFixture = new DatabaseFixture();
WebApplicationFactory = new IntegrationTestWebAppFactory(DbFixture);
Client = WebApplicationFactory.CreateDefaultClient();
}
public Task InitializeAsync() => Task.Completed;
public async Task DisposeAsync()
{
await WebApplicationFactory.DisposeAsync();
await DbFixture.DisposeAsync();
}
}
public class IntegrationTestWebAppFactory : WebApplicationFactory<Program>
{
private readonly DatabaseFixture _dbFixture;
private string _connectionString = null!;
public IntegrationTestWebAppFactory(DatabaseFixture dbFixture)
{
_dbFixture = dbFixture;
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
_connectionString = _dbFixture.ConnectionString.Replace("master", "myDB");
builder.ConfigureTestServices(services => {
// Remove serviços
services.RemoveAll(typeof(DbContextOptions<MyDbContext>));
services.RemoveAll(typeof(DbContextOptions<DbIcalaContext>));
services.RemoveAll(typeof(IOptions<IscalaDbOptions>));
services.AddOptions();
services.AddDbContext<MyDbContext>(opt => {
opt.UseSqlServer(_connectionString);
});
services.AddDbContext<DbIcalaContext>();
var iscalaOptions = new IscalaDbOptions
{
ConnectionString = _connectionString.Replace("myDB", "{BaseERP}"),
};
services.AddSingleton<IOptions<IscalaDbOptions>>(_ => Options.Create(iscalaOptions));
});
}
}
Your test class needs to be decorated with Collection
, not CollectionDefinition
.
[Collection(nameof(SharadTestCollection))]
public class BaseIntegrationTest : IAsyncLifetime
{
protected HostAndDatabaseFixture Fixture { get; }
protected HttpClient Client { get; }
protected IServiceScope Scope { get; }
protected ISender Sender => fixture.Scope.ServiceProvider.GetRequiredService<ISender>();
protected BaseIntegrationTest(HostAndDatabaseFixture fixture)
{
Fixture = fixture;
Client = fixture.Client;
Scope = factory.Services.CreateScope();
}
public virtual Task InitializeAsync() => Task.Completed;
public virtual async Task DisposeAsync()
{
await DbFixture.ResetDataBaseAsync();
Scope.Dispose();
}
}
public class SupplierTests : BaseIntegrationTest
{
public SupplierTests(HostAndDatabaseFixture fixture) : base(fixture)
{
}
[Fact]
public async Task ShuldReturn10()
{
// Arrange
Client.DefaultRequestHeaders
.Add("X-TenantId", "1");
// Act
var suppliers = await Client.GetFromJsonAsync<List<SupplierEntity>>("/api/supplier");
// Assert
Assert.NotNull(fornecedores);
Assert.IsType<List<SupplierEntity>>(fornecedores);
var qtdeSupplier = 10;
Assert.Equal(suppliers.Count, qtdeSupplier);
}
}
Upvotes: 1
Reputation: 3416
The error you are seeing is related to xUnit's dependency injection setup, and your configuration is not supported. Class or collection fixtures only support default constructors. However, the class IntegrationTestWebAppFactory
is an ICollectionFixture<TFixture>
and uses a non-default constructor, which is not allowed.
xUnit.net may not be very explicit about this, but the recommendation is:
If you have need to control creation order and/or have dependencies between fixtures, you should create a class which encapsulates the other two fixtures, so that it can do the object creation itself.
Upvotes: 0