ASP.NET Core 2.1.0-preview1: Functional testing of MVC applications

For ASP.NET Core 2.1 we have created a new package, Microsoft.AspNetCore.Mvc.Testing, to help streamline in-memory end-to-end testing of MVC applications using TestServer.

This package takes care of some of the typical pitfalls when trying to test MVC applications using TestServer.

  • It copies the .deps file from your project into the test assembly bin folder.
  • It sets the content root the application's project root so that static files and views can be found.
  • It provides a class WebApplicationTestFixture<TStartup> that streamlines the bootstrapping of your app on TestServer.

Create a test project

To try out the new MVC test fixture, let's create an app and write an end-to-end in-memory test for the app.

First, create an app to test.

dotnet new razor -au Individual -o TestingMvc/src/TestingMvc

Add an xUnit based test project.

dotnet new xunit -o TestingMvc/test/TestingMvc.Tests

Create a solution file and add the projects to the solution.

cd TestingMvc
dotnet new sln
dotnet sln add src/TestingMvc/TestingMvc.csproj
dotnet sln add test/TestingMvc.Tests/TestingMvc.Tests.csproj

Add a reference from the test project to the app we're going to test.

dotnet add test/TestingMvc.Tests/TestingMvc.Tests.csproj reference src/TestingMvc/TestingMvc.csproj

Add a reference to the Microsoft.AspNetCore.Mvc.Testing package.

dotnet add test/TestingMvc.Tests/TestingMvc.Tests.csproj package Microsoft.AspNetCore.Mvc.Testing -v 2.1.0-preview1-final

In the test project create a test using the WebApplicationTestFixture<TStartup> class that retrieves the home page for the app. Use the test fixture create an HttpClient that allows you to invoke your app in-memory.

using Xunit;

namespace TestingMvc.Tests
{
    public class TestingMvcFunctionalTests : IClassFixture<WebApplicationTestFixture<Startup>>
    {
        public TestingMvcFunctionalTests(WebApplicationTestFixture<Startup> fixture)
        {
            Client = fixture.CreateClient();
        }

        public HttpClient Client { get; }

        [Fact]
        public async Task GetHomePage()
        {
            // Arrange & Act
            var response = await Client.GetAsync("/");

            // Assert
            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        }
    }
}

To correctly invoke your app the test fixture tries to find a static method on the entry point class (typically Program) of the assembly containing the Startup class with the following signature:

public static IWebHostBuilder CreateWebHostBuilder(string [] args)

Fortunately the built-in project templates are already setup this way:

namespace TestingMvc
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateWebHostBuilder(args).Build().Run();
        }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>();
    }
}

If you don't have the Program.CreateWebHostBuilder method the text fixture won't be able to initialize your app correctly for testing. Instead you can configure the WebHostBuilder yourself by overriding CreateWebHostBuilder on WebApplicationTestFixture<TStartup>.

Specifying the app content root

The test fixture will also attempt to guess the content root of the app under test. By convention the test fixture assumes the app content root is at <<SolutionFolder>>/<<AppAssemblyName>>. For example, based on the folder structure defined below, the content root of the application is defined as /work/MyApp.

/work
    /MyApp.sln
    /MyApp/MyApp.csproj
    /MyApp.Tests/MyApp.Tests.csproj

Because we are using a different layout for our projects we need to inherit from WebApplicationTestFixture and pass in the relative path from the solution to the app under test when calling the base constructor. In a future preview we plan to make configuration of the content root unnecessary, but for now this explicit configuration is required for our solution layout.

public class TestingMvcTestFixture<TStartup> : WebApplicationTestFixture<TStartup> where TStartup : class
{
    public TestingMvcTestFixture()
        : base("src/TestingMvc") { }
}

Update the test class to use the derived test fixture.

public class TestingMvcFunctionalTests : IClassFixture<TestingMvcTestFixture<Startup>>
{
    public TestingMvcFunctionalTests(TestingMvcTestFixture<Startup> fixture)
    {
        Client = fixture.CreateClient();
    }

    public HttpClient Client { get; }

    [Fact]
    public async Task GetHomePage()
    {
        // Arrange & Act
        var response = await Client.GetAsync("/");

        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
    }
}

For some end-to-end in-memory tests to work properly, shadow copying needs to be disabled in your test framework of choice, as it causes the tests to execute in a different folder than the output folder. For instructions on how to do this with xUnit see https://xunit.github.io/docs/configuring-with-json.html.

Run the test

Run the test by running dotnet test from the TestingMvc.Tests project directory. It should fail because the HTTP response is a temporary redirect instead of a 200 OK. This is because the app has HTTPS redirection middleware in its pipeline (see Improvements for using HTTPS) and base address setup by the test fixture is an HTTP address ("http://localhost"). The HttpClient by default doesn't follow these redirects. In a future preview we will update the text fixture to configure the HttpClient to follow redirects and also handle cookies. But at least now we know the test is successfully running the app's pipeline.

This test was intended to make simple GET request to the app's home, not test the HTTPS redirect logic, so let's use the fixture to create an HttpClient that uses an HTTPS base address instead.

public TestingMvcFunctionalTests(TestingMvcTestFixture<Startup> fixture)
{
    Client = fixture.CreateClient(new Uri("https://localhost"));
}

Rerun the test and it should now pass.

Starting test execution, please wait...
[xUnit.net 00:00:01.1767971]   Discovering: TestingMvc.Tests
[xUnit.net 00:00:01.2466823]   Discovered:  TestingMvc.Tests
[xUnit.net 00:00:01.2543165]   Starting:    TestingMvc.Tests
[xUnit.net 00:00:09.3860248]   Finished:    TestingMvc.Tests

Total tests: 1. Passed: 1. Failed: 0. Skipped: 0.
Test Run Successful.

Summary

We hope the new MVC test fixture in ASP.NET Core 2.1 will make it easier to reliably test your MVC applications. Please give it a try and let us know what you think on GitHub.