Practical ASP.NET

In-Memory ASP.NET Core Integration Tests with TestServer

Test, test and test again. You can automate those tests with a TestServer-based Web app that doesn't even touch the Web server.

With the advent of ASP.NET Core comes additional automated testing possibilities, such as the ability to write integration tests without needing to deploy the Web app to a Web server.

The TestServer class allows an in-memory test server to be created and HTTP requests issued to the ASP.NET Core Web app. These requests may be routed to view-returning controllers or even Web API controllers that return JSON or other data.

Getting Started
The first step is to add a new test project to the solution containing the ASP.NET Core project, for example by adding a new xUnit.net Test Project. To get access to the TestServer class, the Microsoft.AspNetCore.TestHost NuGet package needs to be installed into the test project. The main Web app project also needs to be referenced from the test project.

You may need to add an additional target to the test project .csproj file if you experience build errors, as shown in here:

<!-- see https://github.com/NuGet/Home/issues/4412 for more info -->
<Target Name="CopyDepsFiles" AfterTargets="Build" Condition="'$(TargetFramework)'!=''">
  <ItemGroup>
  <DepsFilePaths Include="$([System.IO.Path]::ChangeExtension('%(_ResolvedProjectReferencePaths.FullPath)', 
  '.deps.json'))" />
  </ItemGroup>

  <Copy SourceFiles="%(DepsFilePaths.FullPath)" DestinationFolder="$(OutputPath)" 
    Condition="Exists('%(DepsFilePaths.FullPath)')" />
</Target>

Creating a TestServer Instance
Once created, a TestServer instance can create an HttpClient that can be used to issue HTTP requests to the ASP.NET Core Web app.

To create a TestServer instance, a WebHostBuilder can be created and configured. This is essentially describing what the TestServer will be hosting. There are a number of configuration options that can be specified, such as:

  • What is the content root path to the Web app
  • Which environment to run the Web app in
  • Which Startup class to use
  • Additional service configuration, such as setting anti-forgery cookie names
  • Whether to use additional features such as Application Insights

Here's the code to create and configure a WebHostBuilder (note the path to the root of WebApplication1):

var builder = new WebHostBuilder()
  .UseContentRoot(@"C: \WritingCommisions\VSMagazine\TestServer1\src\WebApplication1\WebApplication1")
  .UseEnvironment("Development")
  .UseStartup<WebApplication1.Startup>()
  .UseApplicationInsights();

Once configured, the WebHostBuilder instance can be passed to the TestServer constructor. The CreateClient method can then be called on the TestServer instance to get an HttpClient :

TestServer testServer = new TestServer(builder);
HttpClient client = testServer.CreateClient();

Using the TestServer Instance to make Requests
Once the HttpClient is available, it can be used to make HTTP requests, including GET and POST requests. The response from these requests can then be used to assert that the correct content has been returned.

Listing 1 shows an example of calling the home index view, getting the HTML content as a string, and then asserting the HTML contains the correct content.

Listing 1: Testing HTML Content Results
[Fact]
public async Task DefaultHomePage()
{
  var builder = new WebHostBuilder()
    .UseContentRoot(
            @"C:\WritingCommisions\VSMagazine\TestServer1\src\WebApplication1\WebApplication1")
    .UseEnvironment("Development")
    .UseStartup<WebApplication1.Startup>()
    .UseApplicationInsights();

  TestServer testServer = new TestServer(builder);

  HttpClient client = testServer.CreateClient();

  HttpResponseMessage response = await client.GetAsync("/Home");

  // Fail the test if non-success result
  response.EnsureSuccessStatusCode();

  // Get the response as a string
  string responseHtml = await response.Content.ReadAsStringAsync();

  // Assert on correct content
  Assert.Contains("Home Page", responseHtml);
}

Testing HTTP Posts
In addition to HTTP GETs, HTTP POSTs can also be used with TestServer. For example, the code in the following code shows a simplified action on the HomeController that accepts HTTP posted data:

public class NewRegistrationDetails
{
  public string FirstName { get; set; }
  public string LastName { get; set; }
}

[HttpPost]
public IActionResult Register(NewRegistrationDetails details)
{
  // Code omitted

  return Ok();
}

To send an HTTP Post request, encoded form data can be posted using an HttpRequestMessage as Listing 2 demonstrates.

Listing 2: Testing POST Actions with TestServer
[Fact]
public async Task Post()
{
  var builder = new WebHostBuilder()
    .UseContentRoot(
            @"C:\WritingCommisions\VSMagazine\TestServer1\src\WebApplication1\WebApplication1")
    .UseEnvironment("Development")
    .UseStartup<WebApplication1.Startup>()
    .UseApplicationInsights();

  TestServer testServer = new TestServer(builder);

  HttpClient client = testServer.CreateClient();

            
  var formData = new Dictionary<string, string>
  {
    {"FirstName", "Sarah"},
    {"LastName", "Smith"}     
  };

  HttpRequestMessage postRequest = new HttpRequestMessage(HttpMethod.Post, "Home/Register")
  {
    Content = new FormUrlEncodedContent(formData)
  };


  var response = await client.SendAsync(postRequest);

  response.EnsureSuccessStatusCode();

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

  // Additional asserts could go here 
}

Note that when posting to methods protected with anti-forgery validation there will be errors. A GET would have to be performed first and the anti-forgery token/cookie passed with the subsequent POST request.

The tests could be refactored as Listing 3 shows.

Listing 3: Refactored Tests
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.PlatformAbstractions;
using Xunit;

namespace WebApplication1.Tests
{
  public class IntegrationTestsRefactored
  {
    public HttpClient Client { get; }

    public IntegrationTestsRefactored()
    {            
      var webHostBuilder = new WebHostBuilder()
        .UseContentRoot(CalculateRelativeContentRootPath())
        .UseEnvironment("Development")
        .UseStartup<WebApplication1.Startup>()
        .UseApplicationInsights();

      var testServer = new TestServer(webHostBuilder);

      Client = testServer.CreateClient();

      string CalculateRelativeContentRootPath() => 
        Path.Combine(PlatformServices.Default.Application.ApplicationBasePath, 
           @"..\..\..\..\WebApplication1");            
    }

    [Fact]
    public async Task DefaultHomePage()
    {
      var response = await Client.GetAsync("/Home");

      response.EnsureSuccessStatusCode();

      string responseHtml = await response.Content.ReadAsStringAsync();

      Assert.Contains("Home Page", responseHtml);
    }

    [Fact]
    public async Task Post()
    {
      var formData = new Dictionary<string, string>
      {
        {"FirstName", "Sarah"},
        {"LastName", "Smith"}
      };

      var postRequest = new HttpRequestMessage(HttpMethod.Post, "Home/Register")
      {
        Content = new FormUrlEncodedContent(formData)
      };

      var response = await Client.SendAsync(postRequest);

      response.EnsureSuccessStatusCode();

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

What's happening here is that it centralizes the creation of the TestServer in the test class constructor and also calculates the content root path by using a relative path from the test project to the Web project.

Happy testing!

About the Author

Jason Roberts is a Microsoft C# MVP with over 15 years experience. He writes a blog at http://dontcodetired.com, has produced numerous Pluralsight courses, and can be found on Twitter as @robertsjason.

comments powered by Disqus
Upcoming Events

.NET Insight

Sign up for our newsletter.

I agree to this site's Privacy Policy.