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);
}
}
}
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