ASP.NET MVC CORE web application integration tests with EF Core in-memory database - fresh database for each test

asp.net asp.net-core-mvc entity-framework entity-framework-core integration-testing

Question

I am learning about ASP.NET Core 3 and have built a basic application. I am looking run integration tests to assert calls to the controllers read/write from the database correctly. To avoid having to rely on the actual database I am looking at using EF Core's in-memory database. I have been following this article as my main guide.

The problem I have is that I am struggling to ensure each separate integration test uses a fresh database context.

Initially, I encountered errors calling my database seed method more than once (the second and subsequent calls failed to add a duplicate primary key - essentially it was using the same context).

From looking at various blogs, tutorial and other questions here, I worked around this by instantiating the in-memory database with a unique name (using Guid.NewGuid()). This should have solved my problem. However, this gave me a different issue. The database seed method was correctly called at each test initialisation, however when I then called a controller action the dependency injection instantiated a new database context, meaning that no seed data was present!

I seem to be going in circles either only being able to call seed data once, and only being able to have a single test, or having more than one test but with no seed data!

I have experimented with the scope lifetimes for the DbContext service, setting this to transient/scoped/singleton, but with seemingly no difference in results.

The only way I have managed to get this to work is to add a call to db.Database.EnsureDeleted() before the call to db.Database.EnsureCreated() in the seed method, but this seems like a massive hack and doesn't feel right.

Posted below is my utilities class to set up the in-memory database for the tests, and a test class. Hopefully this is sufficient, as I feel this post is long enough as it is, but the actual controller / startup class can be posted if necessary (though they are fairly vanilla).

Any help much appreciated.

Utilities class to set up the in-memory database

using CompetitionStats.Entities;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Linq;

namespace CompetitionStatsUnitTests
{
    class Utilities
    {
        internal class CustomWebApplicationFactory<TStartup>
            : WebApplicationFactory<TStartup> where TStartup : class
        {
            protected override void ConfigureWebHost(IWebHostBuilder builder)
            {
                builder.ConfigureServices(services =>
                {
                    // Remove the app's ApplicationDbContext registration.
                    var descriptor = services.SingleOrDefault(
                        d => d.ServiceType == typeof(DbContextOptions<CompetitionStatsContext>));

                    if (descriptor != null)
                    {
                        services.Remove(descriptor);
                    }

                    // Add ApplicationDbContext using an in-memory database for testing.
                    services.AddDbContext<CompetitionStatsContext>(options =>
                    {
                        options.UseInMemoryDatabase("InMemoryDbForTesting");
                    });

                    // Build the service provider.
                    var sp = services.BuildServiceProvider();

                    // Create a scope to obtain a reference to the database context (ApplicationDbContext).
                    using (var scope = sp.CreateScope())
                    {
                        var scopedServices = scope.ServiceProvider;
                        var db = scopedServices.GetRequiredService<CompetitionStatsContext>();
                        var logger = scopedServices.GetRequiredService<ILogger<CustomWebApplicationFactory<TStartup>>>();
                        db.Database.EnsureDeleted();  // feels hacky - don't think this is good practice, but does achieve my intention
                        db.Database.EnsureCreated();

                        try
                        {
                            InitializeDbForTests(db);
                        }
                        catch (Exception ex)
                        {
                            logger.LogError(ex, "An error occurred seeding the database with test messages. Error: {Message}}", ex.Message);
                        }
                    }
                });
            }

            private static void InitializeDbForTests(CompetitionStatsContext db)
            {
                db.Teams.Add(new CompetitionStats.Models.TeamDTO
                {
                    Id = new Guid("3b477978-f280-11e9-8490-a8667f2f93c4"),
                    Name = "Arsenal"
                });

                db.SaveChanges();
            }
        }
    }
}

Test class

using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Net.Http;
using System.Threading.Tasks;

namespace CompetitionStatsUnitTests.ControllerUnitTests
{
    [TestClass]
    public class TeamControllerTest
    {
        private HttpClient _testClient;

        [TestInitialize]
        public void Initialize()
        {
            var factory = new Utilities.CustomWebApplicationFactory<CompetitionStats.Startup>();
            this._testClient = factory.CreateClient();
        }

        [TestMethod]
        public async Task TeamController_GetTeam_Returns_Team()
        {
            var actualResponse = await this._testClient.GetStringAsync("api/teams/3b477978-f280-11e9-8490-a8667f2f93c4");
            var expectedResponse = @"{""id"":""3b477978-f280-11e9-8490-a8667f2f93c4"",""name"":""Arsenal""}";
            Assert.AreEqual(expectedResponse, actualResponse);
        }

        [TestMethod]
        public async Task TeamController_PostTeam_Adds_Team()
        {
            var content = new StringContent(@"{""Name"": ""Liverpool FC""}", System.Text.Encoding.UTF8, "application/json");
            var response = await this._testClient.PostAsync("api/teams/", content);
            Assert.AreEqual(response.StatusCode, System.Net.HttpStatusCode.Created);
        }
    }
}
1
1
12/7/2019 1:14:30 PM

Popular Answer

 options.UseInMemoryDatabase("InMemoryDbForTesting");

This creates/uses a database with the name “MyDatabase”. If UseInMemoryDatabase is called again with the same name, then the same in-memory database will be used, allowing it to be shared by multiple context instances.

So you will get the error like{"An item with the same key has already been added. Key: 3b477978-f280-11e9-8490-a8667f2f93c4"} when you add data with the same Id repeatedly

You could add a judgment to the initialization method :

 private static void InitializeDbForTests(CompetitionStatsContext db)
        {
            if (!db.Teams.Any())
            {
                db.Teams.Add(new Team
                {
                    Id = new Guid("3b477978-f280-11e9-8490-a8667f2f93c4"),
                    Name = "Arsenal"
                });
            }

            db.SaveChanges();
        }

You could also refer to the suggestions provided by Grant says adios SE in this thread

1
12/9/2019 11:29:57 AM


Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow