I have multiple ICollectionFixture<WebApplicationFactory>
and use the WebApplicationFactory::InitializeAsync
to initialize the database. However, I run into duplicate PK issue.
I use xUnit with different test collections:
[CollectionDefinition(Name)]
public class ControllerTestsCollection : ICollectionFixture<CustomWebApplicationFactory<Program>>
{
public const string Name = "Controller Test Collection";
}
[CollectionDefinition(Name)]
public class SignalRTestsCollection : ICollectionFixture<CustomWebApplicationFactory<Program>>
{
public const string Name = "SignalR Test Collection";
}
CustomWebApplicationFactory
:
public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup>, IAsyncLifetime where TStartup : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
// Route the application's logs to the xunit output
builder.UseEnvironment("IntegrationTests");
builder.ConfigureLogging((p) => p.SetMinimumLevel(LogLevel.Debug));
builder.ConfigureServices((context, services) =>
{
// Create a new service provider.
services.Configure<GrpcConfig>(context.Configuration.GetSection(nameof(GrpcConfig)));
services.AddScoped<SignInManager<AppUser>>();
});
}
public async ValueTask InitializeAsync()
{
using (var scope = Services.CreateScope())
try
{
var scopedServices = scope.ServiceProvider;
var appDb = scopedServices.GetRequiredService<AppDbContext>();
var identityDb = scopedServices.GetRequiredService<AppIdentityDbContext>();
ILoggerFactory loggerFactory = scopedServices.GetRequiredService<ILoggerFactory>();
ILogger logger = loggerFactory.CreateLogger<CustomWebApplicationFactory<TStartup>>();
// Ensure the database is created.
await appDb.Database.EnsureCreatedAsync();
await identityDb.Database.EnsureCreatedAsync();
// Seed the database with test data.
logger.LogDebug($"{nameof(InitializeAsync)} populate test data...");
await SeedData.PopulateTestData(identityDb, appDb); // XXX: How to synchronize this?
}
catch (Exception ex)
{
Console.WriteLine($"{nameof(InitializeAsync)} exception! {ex}");
throw;
}
}
PopulateTestData
(All values hard-coded):
public static async Task PopulateTestData(AppIdentityDbContext dbIdentityContext, AppDbContext dbContext)
{
AppUser appUser = await dbIdentityContext.Users.FirstOrDefaultAsync(i => i.UserName.Equals("mickeymouse"));
if (appUser == null)
await dbIdentityContext.Users.AddAsync(new AppUser // This fails because it is not atomic.
{
Id = "41532945-599e-4910-9599-0e7402017fbe",
UserName = "mickeymouse",
NormalizedUserName = "MICKEYMOUSE",
Email = "mickey@mouse.com",
NormalizedEmail = "MICKEY@MOUSE.COM",
PasswordHash = "...",
SecurityStamp = "YIJZLWUFIIDD3IZSFDD7OQWG6D4QIYPB",
ConcurrencyStamp = "e432007d-0a54-4332-9212-ca9d7e757275",
FirstName = "Micky",
LastName = "Mouse"
});
The test fails when multiple test collections try to initialize the database. It fails with the following race condition exception:
Collection fixture type 'Web.Api.IntegrationTests.CustomWebApplicationFactory`1[[Program, Web.Api, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]' threw in InitializeAsync
Microsoft.EntityFrameworkCore.DbUpdateException : An error occurred while saving the entity changes. See the inner exception for details.
Npgsql.PostgresException : 23505: duplicate key value violates unique constraint "PK_AspNetUsers"DETAIL: Key ("Id")=(41532945-599e-4910-9599-0e7402017fbe) already exists.
How to properly check and add an entity only if it does NOT exist?
If I run the individual collections separately, it doesn't hit this error / exception. Multiple instances of WebApplicationFactory::InitializeAsync()
call SeedData.PopulateTestData()
which check for existence of an entity before creating it using async/await
pattern. How to better design/implement this logic?
This IS a race condition. Use Assembly Fixtures (https://xunit.net/docs/shared-context#assembly-fixture) or locking mechanism to have an atomic RMW DB operation.