I am building a web application using ASP.NET core. Most of the tests in my application are unit tests, so I mocked every dependencies in order to have fast unit tests. On the other hand I am of the opinion that the integration tests are also very important so end-to-end tests should be written as wel. As for ASP.NET core an end-to-end test is from the controller to the database. Let's see what I have done using ASP.NET core 2.0, with its InMemmory database and XUnit and FluentAssertions framework.
First of all I have made a testing fixture in order to have possibilities for setUp and tearDown before and after each integration test.
public abstract class InMemoryDatabaseFixture : IDisposable { public MokaKukaTrackerDbContext DatabaseContext { get; private set; } protected InMemoryDatabaseFixture() { DatabaseContext = new InMemoryDatabaseInitalizer().Init(); } public void Dispose() { DatabaseContext.Dispose(); } }
The InMemoreDatabaseInitalizer
looks like the following:
public MokaKukaTrackerDbContext Init() { var dbContext = new MokaKukaTrackerDbContext(GetDbContextOptionsWithInMemoryDatabase()); AddTestData(dbContext); return dbContext; } private static void AddTestData(MokaKukaTrackerDbContext dbContext) { AddContainers(dbContext); AddOrders(dbContext); AddLocations(dbContext); AddTruckDrivers(dbContext); dbContext.SaveChanges(); } private static DbContextOptions<MokaKukaTrackerDbContext> GetDbContextOptionsWithInMemoryDatabase() { var optionsBuilder = new DbContextOptionsBuilder<MokaKukaTrackerDbContext>(); var a = optionsBuilder.Options; optionsBuilder.UseInMemoryDatabase(Guid.NewGuid().ToString()); return optionsBuilder.Options; } private static void AddContainers(MokaKukaTrackerDbContext dbContext) { dbContext.Containers.Add(MokaKukaTrackerTestContext.Container1); dbContext.Containers.Add(MokaKukaTrackerTestContext.Container2); dbContext.Containers.Add(MokaKukaTrackerTestContext.Container3); dbContext.Containers.Add(MokaKukaTrackerTestContext.ContainerWithoutOrder); } private static void AddOrders(MokaKukaTrackerDbContext dbContext) { dbContext.Orders.Add(MokaKukaTrackerTestContext.Order1); dbContext.Orders.Add(MokaKukaTrackerTestContext.Order2); dbContext.Orders.Add(MokaKukaTrackerTestContext.Order3); } private static void AddLocations(MokaKukaTrackerDbContext dbContext) { dbContext.Location.Add(MokaKukaTrackerTestContext.Location1); dbContext.Location.Add(MokaKukaTrackerTestContext.Location2); dbContext.Location.Add(MokaKukaTrackerTestContext.Location3); dbContext.Location.Add(MokaKukaTrackerTestContext.Location4); } private static void AddTruckDrivers(MokaKukaTrackerDbContext dbContext) { dbContext.TruckDrivers.Add(MokaKukaTrackerTestContext.TruckDriver1); dbContext.TruckDrivers.Add(MokaKukaTrackerTestContext.TruckDriver2); } }
As you can see, in the InMemoreDatabaseInitalizer
I setup my database context to be InMemory
type. Then I populate my database with some data relating to the business domain. So far so good. Let's see an integration test:
public class OrderToContainerAssignerServiceTest : InMemoryDatabaseFixture { [Fact] public void ThereIsAContainerWithoutOrder_assignOrderToContainer_orderIsAssignedToTheContainer() { //Arrange var containerRepository = new ContainerRepository(DatabaseContext); var orderRepository = new OrderRepository(DatabaseContext); var assigner = new OrderToContainerAssignerService( containerRepository, orderRepository); //Act assigner.Assign(MokaKukaTrackerTestContext.Order1.Id, MokaKukaTrackerTestContext.ContainerWithoutOrder.Id); //Assert var containerWithAssignedOrder = containerRepository.Get(MokaKukaTrackerTestContext.ContainerWithoutOrder.Id); containerWithAssignedOrder.Order.Should().NotBeNull(); containerWithAssignedOrder.Order.NumberOfTurns.Should().Be(1); containerWithAssignedOrder.Order.Status.Should().Be(OrderStatus.Active); } }
I am a bit doubting about my implementation. The problem is, that before every integration test, a new database context is created and all the data is populated there again and again. This database-set-up operation is time expensive and the execution time of the tests is a big factor when running the tests. Or is it not that significant? Or maybe it is better idea to make a global setUp and tearDown before and after all tests? I have also made a solution for that. I have namely made CollectionFixture
:
[CollectionDefinition("Integration Test")] public class GlobalInMemoryDatabaseCollection : ICollectionFixture<InMemoryDatabaseFixture> { // This class has no code, and is never created. Its purpose is simply // to be the place to apply [CollectionDefinition] and all the // ICollectionFixture<> interfaces. // USAGE: Use only the [Collection("Integration Test")] annotation at class level and pass the InMemoryDatabaseFixture fixture in the constructor of the test }
With this fixture we have to add the [Collection("Integration Test")]
annotation at testclass level, and also add a constructor for the given testclass with the databaseFixture
as a parameter (and of course we do not have to extend the fixture anymore).
What are your opinion what is the best approach for handling integration tests in ASP.NET web application? Are these the correct ways for creating integration tests in such an application? Do you have any other recommendation regarding my code?
Before every integration test, a new database context is created and all the data is populated there again and again
is actually a very good thing. You want your integration tests to be as separate as possible, sharing context between them could lead to issues when you decide to run tests in pararell or even synchronously.\$\endgroup\$