In-Memory Integration Testing

Integration tests are an important part of any development pipeline. They add valuable insight into the working state of your components and guard against integration issues prior to deployment.

Integration tests can often provide a challenge as they require developer machines and build agents in the CI pipeline to have all required dependencies running. In this post I will be using a WebAPI that connects to a SQL Server Database as an example.

Historically, to write integration tests for my WebAPI, I would first need a copy of my WebAPI running somewhere. This could be in IIS or within Visual Studio for example. I would also need a copy of SQL Server available to me with a copy of my database setup on it. The database would need to be up-to-date in terms of its schema and be clear of data.

This process is further complicated when it comes to the CI pipeline. Not only would I need to build and unit test my WebAPI, I would then have to deploy a copy of it and the database somewhere so that I can run my integration tests against it. Obviously this also requires resources such as a web server and a database server. Setting up this pipeline can become a cumbersome task.

Microsoft have provided some new tools in recent years which really help to simplify this process. The first is EntityFramework and code first migrations.

Setting up an In-Memory database

Provided you are using code-first migrations, you can simply call the following piece of code to create a copy of your database.

DbContext.Database.EnsureCreated();

This will create the database and apply all migrations, giving you an up-to-date and clean copy of your database. EntityFramework will create this database on whichever server your connection string is directed at.

To enable an in-memory database, we simply need to change the connection string as follows:

Server=(localdb)\mssqllocaldb;Database=EFProviders.InMemory;Trusted_Connection=True;ConnectRetryCount=0;

This will now create the database in-memory. This process is very quick and completes within a few seconds. Obviously this depends on the size of your database but if you are working with micro services then this is likely to be small.

Running a WebAPI in memory using TestServer

Since the release of .NET core we can utilise a class called TestServer. This class allows us to create a copy of a WebAPI that runs in-memory. Furthermore, it is created by pointing it at your Startup.cs file which makes this very powerful. I can guarantee that the in-memory version of my WebAPI will startup in exactly the same way as a deployed version. The following code shows this in action:

var builder = new WebHostBuilder()
    .UseStartup<Startup>();
var testServer = new TestServer(builder) {BaseAddress = new Uri("http://localhost:1234")};

When the above code is executed we will have a copy of the WebAPI, based on the Startup.cs class, running in memory. The API can be accessed on the URI http://localhost:1234.

We can also combine this with the in-memory database using the following code:

var dbContext = testServer.Host.Services.GetService(typeof(YourDbContext)) as YourDbContext;
dbContext.Database.EnsureCreated();

Within a few seconds we will have a both an in-memory database and WebAPI to write tests against. Very cool!

Further enhancements

The WebHostBuilder class used above is very powerful and provides many options for configuring your in-memory WebAPI. Below are some of the key ones that I use:

.UseEnvironment

This option allows you to specify the environment that should be used when launching the WebAPI. For example, you could provide an environment named “IntegrationTests”.

.ConfigureAppConfiguration

Based on the previous option we could provide a appsettings.IntegrationTests.json file with configuration to be used in this environment, such as an in-memory database connection string. For example:

var builder = new WebHostBuilder()
    .UseEnvironment("IntegrationTests")
    .ConfigureAppConfiguration((hostingContext, config) => 
        {
            config.AddJsonFile("appsettings.IntegrationTests.json");
            config.Build();
        });

Overriding Startup and Dependencies

There are often times where your startup class contains code that is not suitable for integration testing. One such example could be the registration of an SMTP mail sender. Let’s say I have a class named SmtpEmailSender for sending emails. This class implements the interface ISmtpEmailSender. When running my integration tests, I do not want real emails to be sent so I would like to override the registration of this component.

A real world example of where I have used this was in a microservice for managing user accounts. When a user registers using the service, an email is sent to the user with an activation link. If I want to test the activation link, I do not want my tests to have to go to an inbox and retrieve the message and activation link. Instead, I overrode the registration of ISmtpEmailSender to register a test version of the class instead. This test class could simply write the contents of the email to a file on disk, rather than actually sending it. I could then pull out the details from the file rather than going to an inbox.

I want the TestServer component to use my Startup.cs class and not a copy of it. This leads to a problem of how to register dependencies differently based on what the service is being used for. My initial solution to this was to check the value of the Environment system variable and register depending on its value. E.g.

if(environment == "IntegrationTests") {
    services.AddTransient<ISmtpEmailSender, TestSmtpEmailSender>();
} else {
  services.AddTransient<ISmtpEmailSender, SmtpEmailSender>();
}

While this is a workable solution, it is ugly and unnecessarily clutters the Startup.cs class. To overcome this, I create a TestStartup.cs class that inherits from my Startup.cs class. I then abstract logic I want to override into virtual methods. The following shows an example Startup.cs class:

public class Startup
    {
        public Startup(IConfiguration configuration, IWebHostEnvironment hostingEnvironment)
        {
        }

        public void ConfigureServices(IServiceCollection services)
        {
            //Register services as usual
            ...
                
            ConfigureSmtpEmailSender(services);
        }

        protected virtual void ConfigureSmtpEmailSender(IServiceCollection services)
        {
            services.AddTransient<ISmtpEmailSender, SmtpEmailSender>();
        }
    }

I can then create a TestStartup.cs class to use in my TestServer as follows:

public class TestStartup : Startup
    {
        public TestStartup(IConfiguration configuration, IWebHostEnvironment hostingEnvironment) 
            : base(configuration, hostingEnvironment)
        {
        }

        protected override void ConfigureSmtpEmailSender(IServiceCollection services)
        {
            services.AddTransient<ISmtpEmailSender, TestSmtpEmailSender>();
        }
    }

Putting it all together

We can combine all of the above to create the in-memory database and test server and provide a platform to write our integration tests against. I use NUnit for my unit and integration tests and the following shows how we can make use of the OneTimeSetUp to instantiate our in-memory components prior to a test run and OneTimeTearDown to clean them up on completion:

public class TestBase
{
    protected MyDbContext Context;
    protected TestServer TestServer;

    [OneTimeSetUp]
    public void Init()
    {
        var builder = new WebHostBuilder()
            .UseEnvironment("IntegrationTests")
            .ConfigureAppConfiguration((hostingContext, config) =>
            {
                config.AddJsonFile("appsettings.IntegrationTests.json");
                config.Build();
            })
            .UseStartup<TestStartup>();
        TestServer = new TestServer(builder) {BaseAddress = new Uri("https://localhost:1234")};

        Context = TestServer.Host.Services.GetService(typeof(MyDbContext)) as MyDbContext;
        Context.Database.EnsureCreated();
    }
    
    [OneTimeTearDown]
    public void CleanUp()
    {
        Context.Database.EnsureDeleted();
        TestServer.Dispose();
    }
}

When I write my tests I can now inherit from this base class, giving me access to the database Context and TestServer. Context can be used just like you would in your repository classes to verify data in the database. To access the WebAPI, TestServer contains a method to provide you with a HttpClient. E.g. TestServer.CreateClient();

The following is an example of a test that implements the base class and uses my Given When Then framework. Read my blog post about this framework here.

[TestFixture]
public partial class AccountEndpointSpecs : TestBase
    {
        [Test]
        public async Task ConfirmEmail_Activates_Account()
        {
            await Given(My_account_already_exists);
            await And(I_have_received_an_email_with_an_activation_link);
            await And(I_construct_a_valid_confirm_email_request);
            await When(I_make_a_call_to_confirm_email);
            await Then(I_should_receive_a_200_response);
            await And(My_account_should_be_confirmed_in_the_database);
        }
    }

Conclusion

In this blog post we have seen the power of using EntityFramework code-first migrations and in-memory integration testing of a WebAPI component. This prevents the need for external dependencies to be present wherever you choose to run the tests.

In my case, I use Azure DevOps as a CI/CD pipeline and this code works perfectly. I use Linux build agents with no trouble at all. Each time my tests run I know that they will start with an empty database and a copy of my WebAPI running in memory, using the same Startup.cs class as the deployed environment.

I hope you have enjoyed reading this post and that the samples above enable you to setup your own, streamlined, integration test pipeline. I would love to hear your thoughts so please feel free to leave me a comment or question below.

You may also like...

Popular Posts