skip to Main Content

I am integration testing an ASP.NET Core app that uses Redis to store some of its state, as well as an SQL databse. My test fixture removes both of these services and replaces them with ones that are more appropriate for testing. My test classes look like this:

public class SignupTest : IClassFixture<CustomWebApplicationFactory<Program>>

where the CustomWebApplicationFactory is:

public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup : class
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            ServiceDescriptor sqlDescriptor = services.Single(
                d => d.ServiceType ==
                     typeof(DbContextOptions<ApiContext>));

            ServiceDescriptor redisDescriptor = services.Single(
                d => d.ServiceType == typeof(IDistributedCache) 
                    && d.ImplementationType?.Name == "RedisCache");

            services.Remove(sqlDescriptor);
            services.Remove(redisDescriptor);
            
            services.AddDbContext<ApiContext>(options =>
            {
                options.UseInMemoryDatabase("InMemoryDbForTesting");
            });

            // Not sure if this is really needed as there seems to be 
            // a fallback IDistributedCache service present by default 
            // (hence the additional narrowing to Name == "RedisCache")
            services.AddDistributedMemoryCache(); 

            ServiceProvider sp = services.BuildServiceProvider();
            using IServiceScope scope = sp.CreateScope();
            IServiceProvider scopedServices = scope.ServiceProvider;

            var db = scopedServices.GetRequiredService<ApiContext>();
            var logger = scopedServices.GetRequiredService<ILogger<CustomWebApplicationFactory<TStartup>>>();
            var cache = scopedServices.GetRequiredService<IDistributedCache>();

            TestUtils.SeedDbForTests(db);
            TestUtils.SeedCacheForTests(cache); // Set values in cache

            logger.LogInformation("Cache session: {string}", cache.GetString("session"));
        });
    }
}

When I debug my tests, I see in the output that the above code is executed and the keys I set in SeedCacheForTests(cache) are retrievable:

MyAPI.Test.Integration.CustomWebApplicationFactory: Information: Cache session: {
    "SessionId": "session_id",
    "DeviceAccountId": "logged_in_id",
    "ViewerId": 10000000001
}

However, in my actual tests it seems that the very same keys I’m setting, and accessing as above, now return null.

This is quite odd since values from the in-memory database DbContext persist just fine. Moreover, if I delete the code that removes Redis or add another Redis instance instead of a DistributedMemoryCache, the tests pass and values are accessible again.

However, using Redis in the fixture pollutes my main instance of Redis, which is undesirable. I am not opposed to using a real Redis instance, so long as it’s possible to keep it separate from the one my app uses at runtime — however I’m on Windows and running Redis normally through Docker, so this could be tricky.

Would very much appreciate any help because I am completely stumped…

2

Answers


  1. Chosen as BEST ANSWER

    As an interim solution, it seems that if I instead call SeedCacheForTests in the constructor of my test class, the values are then able to be accessed:

    public class ToolAuthTest : IClassFixture<CustomWebApplicationFactory<Program>>
    {
        private readonly HttpClient _client;
        private readonly CustomWebApplicationFactory<Program> _factory;
    
        public ToolAuthTest(CustomWebApplicationFactory<Program> factory)
        {
            _factory = factory;
            _client = _factory.CreateClient(new WebApplicationFactoryClientOptions
            {
                AllowAutoRedirect = false
            });
    
            var cache = _factory.Services.GetRequiredService<IDistributedCache>();
            TestUtils.SeedCacheForTests(cache);
        }
    }
    

    However, I really would prefer a way to include this in the fixture as I am looking at adding this code to a large number of tests, and we all dislike boilerplate!


  2. The problem seems to occur when you create a separate ServiceProvider manually, which is different than the "inside factory" one. This makes the singletons to be duplicated (in 2 different service containers), and you are seeding the instance of the IDistributedCache which won’t be used when the api and tests run.

    If you want to get away from seeding in the constructor of the test, if you use xunit, you can take advantage of IAsyncLifetime interface and implement it over your CustomWebApplicationFactory, then you can do the seeding on the InitializeAsync method (which will run after Services are available)

     public Task InitializeAsync()
            {
                DistributedCacheSeed.Seed(Services.GetService<IDistributedCache>());
                return Task.CompletedTask;
            }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search