1

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 Answer 1

2
 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

Sign up to request clarification or add additional context in comments.

1 Comment

Thank you. That suggestion would allow multiple tests to run, but they would not be each using an isolated DB, so I would have to find some way of ordering the tests to make sure they don't interfere with each other, which wouldn't be easily maintainable with a large number of tests.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.