Integration testing in ASP.NET Core

By Steve Smith

Integration testing ensures that an application's components function correctly when assembled together. ASP.NET Core supports integration testing using unit test frameworks and a built-in test web host that can be used to handle requests without network overhead.

View or download sample code (how to download)

Introduction to integration testing

Integration tests verify that different parts of an application work correctly together. Unlike Unit testing, integration tests frequently involve application infrastructure concerns, such as a database, file system, network resources, or web requests and responses. Unit tests use fakes or mock objects in place of these concerns, but the purpose of integration tests is to confirm that the system works as expected with these systems.

Integration tests, because they exercise larger segments of code and because they rely on infrastructure elements, tend to be orders of magnitude slower than unit tests. Thus, it's a good idea to limit how many integration tests you write, especially if you can test the same behavior with a unit test.

Note

If some behavior can be tested using either a unit test or an integration test, prefer the unit test, since it will be almost always be faster. You might have dozens or hundreds of unit tests with many different inputs but just a handful of integration tests covering the most important scenarios.

Testing the logic within your own methods is usually the domain of unit tests. Testing how your application works within its framework, for example with ASP.NET Core, or with a database is where integration tests come into play. It doesn't take too many integration tests to confirm that you're able to write a row to the database and read it back. You don't need to test every possible permutation of your data access code - you only need to test enough to give you confidence that your application is working properly.

Integration testing ASP.NET Core

To get set up to run integration tests, you'll need to create a test project, add a reference to your ASP.NET Core web project, and install a test runner. This process is described in the Unit testing documentation, along with more detailed instructions on running tests and recommendations for naming your tests and test classes.

Note

Separate your unit tests and your integration tests using different projects. This helps ensure you don't accidentally introduce infrastructure concerns into your unit tests and lets you easily choose which set of tests to run.

The Test Host

ASP.NET Core includes a test host that can be added to integration test projects and used to host ASP.NET Core applications, serving test requests without the need for a real web host. The provided sample includes an integration test project which has been configured to use xUnit and the Test Host. It uses the Microsoft.AspNetCore.TestHost NuGet package.

Once the Microsoft.AspNetCore.TestHost package is included in the project, you'll be able to create and configure a TestServer in your tests. The following test shows how to verify that a request made to the root of a site returns "Hello World!" and should run successfully against the default ASP.NET Core Empty Web template created by Visual Studio.

public class PrimeWebDefaultRequestShould
{
    private readonly TestServer _server;
    private readonly HttpClient _client;
    public PrimeWebDefaultRequestShould()
    {
        // Arrange
        _server = new TestServer(new WebHostBuilder()
            .UseStartup<Startup>());
        _client = _server.CreateClient();
    }

    [Fact]
    public async Task ReturnHelloWorld()
    {
        // Act
        var response = await _client.GetAsync("/");
        response.EnsureSuccessStatusCode();

        var responseString = await response.Content.ReadAsStringAsync();

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

This test is using the Arrange-Act-Assert pattern. The Arrange step is done in the constructor, which creates an instance of TestServer. A configured WebHostBuilder will be used to create a TestHost; in this example, the Configure method from the system under test (SUT)'s Startup class is passed to the WebHostBuilder. This method will be used to configure the request pipeline of the TestServer identically to how the SUT server would be configured.

In the Act portion of the test, a request is made to the TestServer instance for the "/" path, and the response is read back into a string. This string is compared with the expected string of "Hello World!". If they match, the test passes; otherwise, it fails.

Now you can add a few additional integration tests to confirm that the prime checking functionality works via the web application:

public class PrimeWebCheckPrimeShould
{
    private readonly TestServer _server;
    private readonly HttpClient _client;
    public PrimeWebCheckPrimeShould()
    {
        // Arrange
        _server = new TestServer(new WebHostBuilder()
            .UseStartup<Startup>());
        _client = _server.CreateClient();
    }

    private async Task<string> GetCheckPrimeResponseString(
        string querystring = "")
    {
        var request = "/checkprime";
        if(!string.IsNullOrEmpty(querystring))
        {
            request += "?" + querystring;
        }
        var response = await _client.GetAsync(request);
        response.EnsureSuccessStatusCode();

        return await response.Content.ReadAsStringAsync();
    }

    [Fact]
    public async Task ReturnInstructionsGivenEmptyQueryString()
    {
        // Act
        var responseString = await GetCheckPrimeResponseString();

        // Assert
        Assert.Equal("Pass in a number to check in the form /checkprime?5",
            responseString);
    }
    [Fact]
    public async Task ReturnPrimeGiven5()
    {
        // Act
        var responseString = await GetCheckPrimeResponseString("5");

        // Assert
        Assert.Equal("5 is prime!",
            responseString);
    }

    [Fact]
    public async Task ReturnNotPrimeGiven6()
    {
        // Act
        var responseString = await GetCheckPrimeResponseString("6");

        // Assert
        Assert.Equal("6 is NOT prime!",
            responseString);
    }
}

Note that you're not really trying to test the correctness of the prime number checker with these tests but rather that the web application is doing what you expect. You already have unit test coverage that gives you confidence in PrimeService, as you can see here:

Test Explorer

You can learn more about the unit tests in the Unit testing article.

Integration testing Mvc/Razor

Test projects that contain Razor views require <PreserveCompilationContext> be set to true in the .csproj file:

    <PreserveCompilationContext>true</PreserveCompilationContext>

Projects missing this element will generate an error similar to the following:

Microsoft.AspNetCore.Mvc.Razor.Compilation.CompilationFailedException: 'One or more compilation failures occurred:
ooebhccx.1bd(4,62): error CS0012: The type 'Attribute' is defined in an assembly that is not referenced. You must add a reference to assembly 'netstandard, Version=2.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51'.

Refactoring to use middleware

Refactoring is the process of changing an application's code to improve its design without changing its behavior. It should ideally be done when there is a suite of passing tests, since these help ensure the system's behavior remains the same before and after the changes. Looking at the way in which the prime checking logic is implemented in the web application's Configure method, you see:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.Run(async (context) =>
    {
        if (context.Request.Path.Value.Contains("checkprime"))
        {
            int numberToCheck;
            try
            {
                numberToCheck = int.Parse(context.Request.QueryString.Value.Replace("?", ""));
                var primeService = new PrimeService();
                if (primeService.IsPrime(numberToCheck))
                {
                    await context.Response.WriteAsync($"{numberToCheck} is prime!");
                }
                else
                {
                    await context.Response.WriteAsync($"{numberToCheck} is NOT prime!");
                }
            }
            catch
            {
                await context.Response.WriteAsync("Pass in a number to check in the form /checkprime?5");
            }
        }
        else
        {
            await context.Response.WriteAsync("Hello World!");
        }
    });
}

This code works, but it's far from how you would like to implement this kind of functionality in an ASP.NET Core application, even as simple as this is. Imagine what the Configure method would look like if you needed to add this much code to it every time you add another URL endpoint!

One option to consider is adding MVC to the application and creating a controller to handle the prime checking. However, assuming you don't currently need any other MVC functionality, that's a bit overkill.

You can, however, take advantage of ASP.NET Core middleware, which will help us encapsulate the prime checking logic in its own class and achieve better separation of concerns in the Configure method.

You want to allow the path the middleware uses to be specified as a parameter, so the middleware class expects a RequestDelegate and a PrimeCheckerOptions instance in its constructor. If the path of the request doesn't match what this middleware is configured to expect, you simply call the next middleware in the chain and do nothing further. The rest of the implementation code that was in Configure is now in the Invoke method.

Note

Since the middleware depends on the PrimeService service, you're also requesting an instance of this service with the constructor. The framework will provide this service via Dependency Injection, assuming it has been configured, for example in ConfigureServices.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using PrimeWeb.Services;
using System;
using System.Threading.Tasks;

namespace PrimeWeb.Middleware
{
    public class PrimeCheckerMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly PrimeCheckerOptions _options;
        private readonly PrimeService _primeService;

        public PrimeCheckerMiddleware(RequestDelegate next,
            PrimeCheckerOptions options,
            PrimeService primeService)
        {
            if (next == null)
            {
                throw new ArgumentNullException(nameof(next));
            }
            if (options == null)
            {
                throw new ArgumentNullException(nameof(options));
            }
            if (primeService == null)
            {
                throw new ArgumentNullException(nameof(primeService));
            }

            _next = next;
            _options = options;
            _primeService = primeService;
        }

        public async Task Invoke(HttpContext context)
        {
            var request = context.Request;
            if (!request.Path.HasValue ||
                request.Path != _options.Path)
            {
                await _next.Invoke(context);
            }
            else
            {
                int numberToCheck;
                if (int.TryParse(request.QueryString.Value.Replace("?", ""), out numberToCheck))
                {
                    if (_primeService.IsPrime(numberToCheck))
                    {
                        await context.Response.WriteAsync($"{numberToCheck} is prime!");
                    }
                    else
                    {
                        await context.Response.WriteAsync($"{numberToCheck} is NOT prime!");
                    }
                }
                else
                {
                    await context.Response.WriteAsync($"Pass in a number to check in the form {_options.Path}?5");
                }
            }
        }
    }
}

Since this middleware acts as an endpoint in the request delegate chain when its path matches, there is no call to _next.Invoke when this middleware handles the request.

With this middleware in place and some helpful extension methods created to make configuring it easier, the refactored Configure method looks like this:

        IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UsePrimeChecker();

        app.Run(async (context) =>
        {
            await context.Response.WriteAsync("Hello World!");
        });
    }
}

Following this refactoring, you're confident that the web application still works as before, since your integration tests are all passing.

Note

It's a good idea to commit your changes to source control after you complete a refactoring and your tests pass. If you're practicing Test Driven Development, consider adding Commit to your Red-Green-Refactor cycle.

Resources