In-memory testing using ASP.NET Core

Posted on Tuesday, 06 Dec 2016

A fundamental problem with integration testing over finer-grained tests such as unit testing is that in order to integration test your component or application you need to spin up a running instance of your application so you can reach it over HTTP, run your tests and then spin it down afterwards.

Spinning up instances of your application can lead to a lot of additional work when it comes to running your tests within any type of continuous deployment or delivery pipeline. This has certainly become easier with the introduction of the cloud, but still requires a reasonable investment of time and effort to setup, as well slowing down your deployment/delivery pipeline.

An alternative approach to running your integration or end-to-end tests is to utilise in-memory testing. This is where your application is spun up in memory via an in-memory server and has the tests run against it. An additional benefit to running your tests this way is you're no longer testing any of your host OS's network stack either (which in most cases will be configured differently to your production server's stack anyway).

TestServer package

Thankfully in-memory testing can be performed easily in ASP.NET Core thanks to the Microsoft.AspNetCore.TestHost NuGet package.

Let's take a moment to look at the TestServer API exposed by the TestHost library:

public class TestServer : IServer, IDisposable
{
    public TestServer(IWebHostBuilder builder);

    public Uri BaseAddress { get; set; }
    public IWebHost Host { get; }

    public HttpClient CreateClient();
    public HttpMessageHandler CreateHandler();
    
    public RequestBuilder CreateRequest(string path);
    public WebSocketClient CreateWebSocketClient();
    public void Dispose();
}

As you'll see, the API has all the necessary endpoints we'll need to spin our application up in memory.

For those that are regular readers of my blog, you'll remember we used the same TestServer package to run integration tests on middleware back in July. This time we'll be using it to run our Web API application in memory and run our tests against it. We'll then assert that the response received is expected.

Enough talk, let's get started.

Running Web API in memory

Setting up Web API

In this instance I'm going to be using ASP.NET Core Web API. In my case I've created a small Web API project using the ASP.NET Core yoman project template. You'll also note that I've stripped a few things out of the application for the sake of making the post as easier to follow. Here are the few files that really matter:

Startup.cs (nothing out of the ordinary here)

public class Startup
{
    public Startup(IHostingEnvironment env)
    {
        var builder = new ConfigurationBuilder()
            .SetBasePath(env.ContentRootPath)
            .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
            .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
            .AddEnvironmentVariables();
        Configuration = builder.Build();
    }

    public IConfigurationRoot Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        // Add framework services.
        services.AddMvc();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        loggerFactory.AddConsole(Configuration.GetSection("Logging"));
        loggerFactory.AddDebug();

        app.UseMvc();
    }
}

ValuesController.cs

[Route("api/[controller]")]
public class ValuesController : Controller
{
    [HttpGet]
    public string Get()
    {
        return "Hello World!";
    }
}

All we've got here is a simple WebAPI application that returns a single Hello World! value from ValueController when you fire a GET request to /api/values/.

Running Web API in memory

At this point I've created a test project alongside my Web API one and added the Microsoft.AspNetCore.TestHost package to my test project's package.json file.

package.json

...
"dependencies": {
    "dotnet-test-xunit": "2.2.0-preview2-build1029",
    "xunit": "2.2.0-beta2-build3300",
    "Microsoft.AspNetCore.TestHost": "1.0.0",
    "TestWebAPIApplication":{
        "target":"project"
    }
},
...

Next, we'll create our first test class, and bootstrap our WebAPI project. Pay particular attention to our web application's Startup.cs that's being passed into the WebHostBuilder's UseStartup<T> method. You'll notice this is exactly the same way we bootstrap our application within Program.cs (the bootstrap file we use when deploying our application).

public class ExampleTestClass
{
    private IWebHostBuilder CreateWebHostBuilder(){
        var config = new ConfigurationBuilder().Build();
        
        var host = new WebHostBuilder()
            .UseConfiguration(config)
            .UseStartup<Startup>();

        return host;
    }

    ...
}

Writing our test

At this point we're ready to write our test, so let's create a new instance of TestServer which takes and instance of IWebHostBuilder.

public TestServer(IWebHostBuilder builder);

As you can see from the following trivial example, we're simply capturing the response from the controller invoked when calling /api/values, which is our case is the ValuesController.

[Fact]
public async Task PassingTest()
{
    var webHostBuilder = CreateWebHostBuilder();
    var server = new TestServer(webHostBuilder);

    using(var client = server.CreateClient()){
        var requestMessage = new HttpRequestMessage(new HttpMethod("GET"), "/api/values/");
        var responseMessage = await client.SendAsync(requestMessage);

        var content = await responseMessage.Content.ReadAsStringAsync();

        Assert.Equal(content, "Hello World!");
    }
}

Now, when we call Assert.Equals on our test we should see the test has passed.

Running test UnitTest.Class1.PassingTest...
Test passed 

Conclusion

Hopefully this post has given you enough insight into how you can run your application in memory for purposes such as integration or feature testing. Naturally there's a lot more you could do to simplify and speed up the tests by limiting the number of times TestServer is created.

Back