blog post image
Andrew Lock avatar

Andrew Lock

~9 min read

Introduction to integration testing with xUnit and TestServer in ASP.NET Core

Most developers understand the value of unit testing and the importance of having a large test base for non-trivial code. However it is also important to have at least some integration tests which confirm that the various parts of your app are functioning together correctly.

In this post I'm going to demonstrate how to create some simple integration tests using xUnit and TestServer for testing the custom middleware shown in my previous post. I'm using the terms functional/integration testing pretty interchangeably here to refer to tests that cover the whole infrastructure stack.

This post was originally written use preview 1 of the tooling. For preview 2 use the following package versions:

  • global.json SDK version - 1.0.0-preview2-003121
  • xunit - 2.2.0-beta2-build3300
  • dotnet-test-xunit - 2.2.0-preview2-build1029
  • Microsoft.NETCore.App and Microsoft.AspNetCore.TestHost - 1.0.0

Creating an integration test project

In order to run your integration tests, you will need to add a test project to your solution. By convention your test projects should reside in a subfolder, test, of the root folder. You may also need to update your global.json to account for this:

{
  "projects": [ "src", "test" ],
  "sdk": {
    "version": "1.0.0-preview1-002702"
  }
}

There are multiple ways to create a new project but all that is required is a project.json in your project folder, which you can create using dotnet new.

You then need to add a dependency to the project under test - in this case NetEscapades.AspNetCore.SecurityHeaders - and the necessary xUnit testing libraries. We also add a reference to Microsoft.AspNetCore.TestHost.

{
    "version": "1.0.0-*",
    "dependencies": {
        "dotnet-test-xunit": "1.0.0-rc2-*",
        "xunit": "2.1.0",
        "Microsoft.AspNetCore.TestHost": "1.0.0-rc2-final",
        "NetEscapades.AspNetCore.SecurityHeaders": {
            "version": "1.0.0-*",
            "target": "project"
        }
    },
    "testRunner": "xunit",
    "frameworks": {
        "netcoreapp1.0": {
            "dependencies": {
                "Microsoft.NETCore.App": {
                    "version": "1.0.0-rc2-3002702",
                    "type": "platform"
                }
            },
            "imports": [
                "dnxcore50",
                "portable-net451+win8"
            ]
        },
        "net451": { }
    }
}

We have added our project under test with the "target": "project" attribute - this ensures that when dependencies are restored, you don't accidentally restore a package from NuGet with the same name. You can also see we are targeting two frameworks, the net451 and netcoreapp1.0, where the latter additionally has a dependency on the platform Microsoft.NETCore.App.

Configuring the TestServer

ASP.NET Core includes a TestServer class in the Microsoft.AspNetCore.TestHost library which can be used to simulate ASP.NET Core applications for testing purposes. We can use this to test the whole ASP.NET Core pipeline, without having to worry about spinning up an actual test website in a different process to test against.

The TestServer class is configured by passing an IWebHostBuilder in the constructor. There are many ways to configure the IWebHostBuilder and various ways to use the configured TestServer, two of which I'll cover here.

Direct configuration in code

With this approach, we directly create a WebHostBuilder in code, and configure it as required for testing our middleware:

var policy = new SecurityHeadersPolicyBuilder()
    .AddDefaultSecurePolicy();

var hostBuilder = new WebHostBuilder()
    .ConfigureServices(services => services.AddSecurityHeaders())
    .Configure(app =>
    {
        app.UseSecurityHeadersMiddleware(policy);
        app.Run(async context =>
        {
            await context.Response.WriteAsync("Test response");
        })
    });

Here we configure the bare minimum we require to setup our app - we setup our SecurityHeadersPolicy, add the required services for our custom middleware, and setup the WebHostBuilder to call our middleware. Finally, the app will return the content string "Test response" for all requests.

Next, we configure our test to create a TestServer object, passing in our WebHostBuilder. We can use this to create and send requests through the ASP.NET Core pipeline.

using (var server = new TestServer(hostBuilder))
{
    var response = await server.CreateRequest("/")
        .SendAsync("GET");

    // Assert 
    response.EnsureSuccessStatusCode();
    var content = await response.Content.ReadAsStringAsync();

    Assert.Equal("Test response", content);
}

In this case we create a request to the root of the web application, send it, and read the content asynchronously. The TestServer object handles resolving and creating the necessary services and middleware, just as it would if it were a normal ASP.NET Core application.

We have now all the pieces we need to perform our integration test, all we need to do is put it together to create our xUnit [Fact] test:

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.TestHost;
using NetEscapades.AspNetCore.SecurityHeaders.Infrastructure;
using Xunit;

namespace NetEscapades.AspNetCore.SecurityHeaders
{
    public class SecurityHeadersMiddlewareTests
    {
        [Fact]
        public async Task DefaultSecurePolicy_RemovesServerHeader()
        {
            // Arrange
            var policy = new SecurityHeadersPolicyBuilder()
                .AddDefaultSecurePolicy();
                
            var hostBuilder = new WebHostBuilder()
                .ConfigureServices(services => services.AddSecurityHeaders())
                .Configure(app =>
                {
                    app.UseSecurityHeadersMiddleware(policy);
                    app.Run(async context =>
                    {
                        await context.Response.WriteAsync("Test response");
                    });
                });

            using (var server = new TestServer(hostBuilder))
            {
                // Act
                // Actual request.
                var response = await server.CreateRequest("/")
                    .SendAsync("GET");

                // Assert
                response.EnsureSuccessStatusCode();
                var content = await response.Content.ReadAsStringAsync();

                Assert.Equal("Test response", content);
                Assert.False(response.Headers.Contains("Server"), "Should not contain server header");
            }
        }
    }
}

We have added an extra assert here to check that our SecurityHeadersMiddleware is correctly removing the "Server" tag.

Using a Startup configuration file

We have shown how to create a TestServer using a manually created WebHostBuilder. This is useful for testing our middleware in various scenarios, but it does not necessarily test a system as it is expected to be used in production.

An alternative to this is to have a separate ASP.NET Core website configured to use our middleware, which we can run using dotnet run. We can then configure our TestServer class to directly use the test website's Startup.cs class for configuration, to ensure our integration tests match the production system as closesly as possible.

Rather than add our TestServer configuration to the body of our tests in this case, we will instead create a helper TestFixture class which we will use to initialise our tests.

public class TestFixture<TStartup> : IDisposable where TStartup : class
{
    private readonly TestServer _server;

    public TestFixture()
    {
        var builder = new WebHostBuilder().UseStartup<TStartup>();
        _server = new TestServer(builder);

        Client = _server.CreateClient();
        Client.BaseAddress = new Uri("http://localhost:5000");
    }

    public HttpClient Client { get; }

    public void Dispose()
    {
        Client.Dispose();
        _server.Dispose();
    }
}

We use the UseStartup<T>() extension method to tell the WebHostBuilder to use our test system's Startup class to configure the application. We then create the TestServer object, and from this we obtain an HttpClient which we can use to send our requests to the TestServer. Finally we set the base address URL that the server will be bound to.

In our test fixture we can implement the IClassFixture<TestFixture> interface to gain access to an instance of the TestFixture class, which xUnit will create and inject into the constructor automatically. We can then use the HttpClient it exposes to run tests against our TestServer.

public class MiddlewareIntegrationTests : IClassFixture<TestFixture<SystemUnderTest.Startup>>
{
    public MiddlewareIntegrationTests(TestFixture<SystemUnderTest.Startup> fixture)
    {
        Client = fixture.Client;
    }

    public HttpClient Client { get; }

    [Theory]
    [InlineData("GET")]
    [InlineData("HEAD")]
    [InlineData("POST")]
    public async Task AllMethods_RemovesServerHeader(string method)
    {
        // Arrange
        var request = new HttpRequestMessage(new HttpMethod(method), "/");

        // Act
        var response = await Client.SendAsync(request);

        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        var content = await response.Content.ReadAsStringAsync();

        Assert.Equal("Test response", content);
        Assert.False(response.Headers.Contains("Server"), "Should not contain server header");
    }
}

We now have a test configured and running using the same Startup class as our website project SystemUnderTest. It's worth noting that although we have a TestServer configured as part of the test, it is not actually listening globally - navigating to http://localhost:5000 in a browser while running the test will timeout and not call in to the configured pipeline.

Running the tests

To run our tests we must first ensure our dependencies are installed by calling dotnet restore in the solution folder. We can then run this test by calling dotnet test in the root of the test project folder. The test project and other required projects will then be compiled and the tests run, which hopefully should give output similar to the following (depending on which platform you're running on):

Project NetEscapades.AspNetCore.SecurityHeaders.Tests (.NETCoreApp,Version=v1.0) was previously compiled. Skipping compilation.
xUnit.net .NET CLI test runner (64-bit .NET Core win10-x64)
  Discovering: NetEscapades.AspNetCore.SecurityHeaders.Tests
  Discovered:  NetEscapades.AspNetCore.SecurityHeaders.Tests
  Starting:    NetEscapades.AspNetCore.SecurityHeaders.Tests
  Finished:    NetEscapades.AspNetCore.SecurityHeaders.Tests
=== TEST EXECUTION SUMMARY ===
   NetEscapades.AspNetCore.SecurityHeaders.Tests  Total: 2, Errors: 0, Failed: 0, Skipped: 0, Time: 0.281s

Project NetEscapades.AspNetCore.SecurityHeaders.Tests (.NETFramework,Version=v4.5.1) was previously compiled. Skipping
compilation.
xUnit.net .NET CLI test runner (64-bit Desktop .NET win10-x64)
  Discovering: NetEscapades.AspNetCore.SecurityHeaders.Tests
  Discovered:  NetEscapades.AspNetCore.SecurityHeaders.Tests
  Starting:    NetEscapades.AspNetCore.SecurityHeaders.Tests
  Finished:    NetEscapades.AspNetCore.SecurityHeaders.Tests
=== TEST EXECUTION SUMMARY ===
   NetEscapades.AspNetCore.SecurityHeaders.Tests  Total: 2, Errors: 0, Failed: 0, Skipped: 0, Time: 0.257s
SUMMARY: Total: 2 targets, Passed: 2, Failed: 0.

Note that as we targeted both net451 and netcoreapp1.0 in our project.json, the tests are compiled and run once on each platform. A test execution summary is produced for each, with each platform showing the number of tests discovered and run. The final summary shows the number of platforms that passed and failed.

Note, there is currently a bug in the test runner of linux/OSX where you cannot run tests on both .NET Core and Mono. The provided code will run correctly on Windows, and the .NET Core tests run successfully, but the net451 tests will not run as described here.

Integration tests over HTTPS

We have successfully configured and run our integration tests using the startup class of our test website. But what if we want to test the behaviour over SSL? Well the simple answer is that we don't necessarily need to - it depends what we are trying to achieve.

For the middleware tests I was running, I needed to verify that the Strict-Transport-Security header would only be added when running over SSL. This is relatively easy to do, by just updating the TestServer and HttpClient to use an HTTPS base url.

So for the TestFixture approach we just need to change the Client.BaseAddress uri.

Client.BaseAddress = new Uri("https://localhost:5001");

Where we configure the 'WebHostBuilder' in code, we add a call to UseUrls() and update the server BaseAddress:

var hostBuilder = new WebHostBuilder()
    .UseUrls("https://localhost:5001")
    .ConfigureServices(services => services.AddSecurityHeaders())
    .Configure(app =>
    {
        app.UseSecurityHeadersMiddleware(policy);
        app.Run(async context =>
        {
            await context.Response.WriteAsync("Test response");
        });
    });

using (var server = new TestServer(hostBuilder))
{
    server.BaseAddress = new Uri("https://localhost:5001");
    ...
}

The pipeline will then proceed as though the request was made over https.

However, it's important to note with this approach that the request is not actually being made over SSL. If you call dotnet run on the test project you will find you can't establish a secure connection, as we haven't actually configured a certificate.

Timeout trying to establish secure conntection

Again, this is not actually required given the way the TestServer works, but for completion sake, I'll describe how to add a certificate to your test project.

Configuring Kestral to use SSL

First, we'll need an SSL certificate. For simplicity, I created a self-signed certificate using IIS - this is fine for testing purposes but you'll run into issues if you try to use it in production. I saved this in a tools subfolder in the solution folder, with the name democertificate.pfx and password democertificate.

Next, we configure the test website to use SSL. The project.json file is updated with the Microsoft.AspNetCore.Server.Kestrel.Https package and we run dotnet restore:

{
 "dependencies": {
    "NetEscapades.AspNetCore.SecurityHeaders": {
        "version": "1.0.0-*",
        "target": "project"
     },
    "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0-rc2-final",
    "Microsoft.AspNetCore.Server.Kestrel": "1.0.0-rc2-final",
    "Microsoft.AspNetCore.Server.Kestrel.Https": "1.0.0-rc2-final"
  },
}

Finally, we update the static void Main() entry point to use our new certificate when initialising the app.

public static void Main(string[] args)
{
    var certificatePath = Path.GetFullPath(
        Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "tools", "democertificate.pfx"));

    var host = new WebHostBuilder()
        .UseKestrel(options => options.UseHttps(certificatePath, "democertificate"))
        .UseUrls("http://localhost:5000", "https://localhost:5001")
        .UseIISIntegration()
        .UseStartup<Startup>()
        .Build();

    host.Run();
}

First we build the path (using Path.Combine to avoid environment specific path separators so it works correctly in Windows and linux) to the self-signed certificate. This is passed to the extension method UseHttps and the urls are updated to listen to both HTTP and HTTPS.

With that, we are able to make requests to our test app over SSL, though we will get a warning about the certificate being self-signed.

Successful request over SSL using self-signed certificate

Obviously there is no reason why you can't configure your integration tests with the same demo certificate. However as demonstrated previously, that's generally not actually required to test most of the behaviour of requests.

Summary

This post demonstrated how to create a test project to perform functional and integration tests with xUnit and the TestServer class in the Microsoft.AspNetCore.TestHost package. It showed two ways to build up the test server, including using the Startup class of a test website. This allows you to send requests and receive responses as though running the app using host.Run(). Finally, it showed how to perform integration tests over SSL. Happy testing!

Andrew Lock | .Net Escapades
Want an email when
there's new posts?