asp.net-coreasync-awaitentity-framework-corexunit.net

Run DB setup once before ALL tests using multiple ICollectionFixture<WebApplicationFactory>?


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?


Solution

  • 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.