Skip to content

How to customize WebApplicationFactory shared among different ICollectionFixtures? #36063

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
khteh opened this issue May 11, 2025 · 0 comments

Comments

@khteh
Copy link

khteh commented May 11, 2025

How do I customize the WebApplicationFactory::InitializeAsync so that only the first collection which runs gets to initialize the database?

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 = "[email protected]",
                NormalizedEmail = "[email protected]",
                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:

  Message: 
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 existance of an entity before creating it using async/await pattern. How to better design/implement this logic?

@khteh khteh changed the title Race conditions in EntityFramework Core FirstOrDefaultAsync() followed by AddAsync used in xUnit InitializeAsync How to customize WebApplicationFactory shared among different ICollectionFixtures? May 12, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant