2
\$\begingroup\$

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?

\$\endgroup\$
  • 2
    \$\begingroup\$ 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\$ – MaLiN2223 Oct 29 '17 at 13:47
4
\$\begingroup\$

I went through the very same conflict and eventually looked at what other frameworks were doing. What you'll find with the above implementation is that it is okay for a couple hundred tests, but soon it will become unbearable slow. What I started doing was wrapping my integration tests in transactions. I've written a blog post that goes into the details here. Some of the highlights:

The alternative that I would like to propose is transactional testing. Transactional tests are tests that get wrapped in a database transaction and are rolled back when the test completes. We can run our tests in an isolated Entity Framework transaction.

This could be as simple as:

public async Task CreateAsync_Persists_Article()
{
    using (var transaction = _dbContext.Database.BeginTransaction())
    {
        try
        {
            // arrange
            var article = ArticleFactory.Get();
            var service = new ArticleService(_dbContext);

            // act
            await service.CreateAsync(article); // will call _dbContext.SaveChanges();

            // assert
            var dbArticle = await _dbContext.Articles.Single();
            Assert.NotNull(dbArticle);
        }
        finally
        {
            transaction.Rollback();
        }
    }
}

But, you can also extract it to an inherited fixture in xUnit. This might look like:

public class DbContextFixture : IDisposable
{
    protected IDbContextTransaction Transaction { get; }
    protected AppDbContext DbContext { get; }

    public DbContextFixture()
    {
        // configure our database
        var options = new DbContextOptionsBuilder<AppDbContext>()
            .UseNpgsql(/* ... */)
            .Options;

        DbContext = new AppDbContext(options);

        // begin the transaction
        Transaction = DbContext.Database.BeginTransaction();
    }

    public void Dispose()
    {
        if (Transaction != null)
        {
            Transaction.Rollback();
            Transaction.Dispose();
        }
    }
}

public class TestClass : DbContextFixture { /* ... */ }

This also lets you use your actual database provider which can validate things like referential integrity and data truncation.

Respawn is another option that claims:

Respawn is a small utility to help in resetting test databases to a clean state. Instead of deleting data at the end of a test or rolling back a transaction, Respawn resets the database back to a clean checkpoint by intelligently deleting data from tables.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.