Testing ASP.NET Core MVC web apps in-memory

Daniel Roth

This post was written and submitted by Javier Calvarro Nelson, a developer on the ASP.NET Core MVC team

Testing is an important part of the development process of any application. In this blog post we are going to explore how we can test ASP.NET Core MVC applications using an in-memory server. This approach has several advantages:

  • It’s very fast because it does not start a real server.
  • It’s reliable because there is no need to reserve ports or clean up resources after it runs.
  • It’s easier than other ways of testing your application, such as using an external test driver.
  • It allows testing of traits in your application that are hard to unit test, like ensuring your authorization rules are correct.

The main shortcoming of this approach is that is not well suited to test applications that heavily rely on JavaScript. That said, if you are writing a traditional web application or an API then all the benefits mentioned above apply.

For testing MVC applications we are going to use TestServer. TestServer is an in-memory implementation of a server for ASP.NET Core applications akin to Kestrel or HTTP.SYS.

Creating and setting up the projects

We are going to start by creating an MVC application using the following command:

dotnet new mvc -au Individual -uld --use-launch-settings -o .\TestingMVC\src\TestingMVC

Next we are going to create a test project with the following command:

dotnet new xunit -o .\TestingMVC\test\TestingMVC.Tests

Next we are going to create a solution, add the projects to the solution and add a reference to the application project from the test project.

dotnet new sln
dotnet sln add .\src\TestingMVC\TestingMVC.csproj
dotnet sln add .\test\TestingMVC.Tests\TestingMVC.Tests.csproj
dotnet add .\test\TestingMVC.Tests\TestingMVC.Tests.csproj reference .\src\TestingMVC\TestingMVC.csproj

The next step is to add references to the components we are going to use for testing. For that, we go to the testing csproj file and add the following item group

 <ItemGroup>
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.3.0" />
    <PackageReference Include="xunit" Version="2.3.0" />
    <PackageReference Include="xunit.runner.visualstudio" Version="2.3.0" />
    <PackageReference Include="Microsoft.AspNetCore.TestHost" Version="2.0.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.0" PrivateAssets="All" />
    <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.0.0" PrivateAssets="All" />
  </ItemGroup>

Now, we can run dotnet restore on the project or the solution and we can move on to writing tests.

Writing a test to retrieve the page at ‘/’

Now that we have our projects set up, we can write a test that will serve as an example of how other tests will look.

We are going to start by changing Program.cs in our application project to look like this:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

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>();
    }
}

In the snippet above, we’ve changed the method IWebHost BuildWebHost(string[] args) to IWebHostBuilder CreateWebHostBuilder(string[] args) and called Build() within the Main(string[] args) method instead. The reason for this is that we want to allow our tests to configure the IWebHostBuilder in the same way the application does and to allow making changes required by tests. (By chaining calls on the WebHostBuilder.)

One example of this will be setting the ContentRoot of the application when we are running the server in a test. The ContentRoot needs to be based on the appliation’s root, not the test’s root.

Now, we can create a test like the one below to get the contents of our home page. This test will fail because we are missing a couple of things that we describe below.

using System;
using System.IO;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using Xunit;

namespace TestingMVC.Tests
{
    public class BasicTests
    {
        [Fact]
        public async Task CanGetHomePage()
        {
            // Arrange
            var webHostBuilder = Program.CreateWebHostBuilder(Array.Empty<string>())
                .UseContentRoot(Path.GetFullPath("../../../../../src/TestingMVC"));

            var server = new TestServer(webHostBuilder);
            var client = server.CreateClient();

            // Act
            var response = await client.GetAsync("/");

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

The test above can be decomposed into the following actions:

  • Create an IWebHostBuilder in the same way that my application creates it.
  • Override the content root of the application to point to the application’s project root instead of the bin folder of the test application. (.\src\TestingMVC instead of .\test\TestingMvc.Tests\bin\Debug\netcoreapp2.0)
  • Create a test server from the WebHost builder.
  • Create an HTTP Client that can be used to communicate with our app. (This uses an internal mechanism that sends the requests in-memory – no network involved.)
  • Send an HTTP request to the server using the client.
  • Ensuring the status code of the response is correct.

Requirements for Razor views to run on a test context

If we tried to run the test above, we will probably get an HTTP 500 error instead of an HTTP 200 success. The reason for this is that the dependency context of the app is not correctly set up in our tests. In order to fix this, there are a few actions we need to take:

  • Copy the .deps.json file from our application to the bin folder of the testing project.
  • Disable shadow copying assemblies.

For the first bullet point, we can create a target file like the one below and include in our testing csproj file as follows:

 <Import Project=".\build\testing.targets" />
<Project>
  <PropertyGroup>
    <!--
      The functional tests act as the host application for all test websites. Since the CLI copies all reference
      assembly dependencies in websites to their corresponding bin/{config}/refs folder we need to re-calculate
      reference assemblies for this project so there's a corresponding refs folder in our output. Without it
      our websites deps files will fail to find their assembly references.
    -->

    <PreserveCompilationContext>true</PreserveCompilationContext>
  </PropertyGroup>

  <Target Name="CopyAditionalFiles" 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>
</Project>

For the second bullet point, the implementation is dependent on what testing framework we use. For xUnit, add an xunit.runner.json file in the root of the test project (set it to Copy Always) like the one below:

{
  "shadowCopy": false
}

This step is subject to change at any point; for more information look at the xUnit docs at http://xunit.github.io/#documentation.

Now if you re-run the sample test, it will pass.

Summary

  • We’ve seen how to create in-memory tests for an MVC application.
  • We’ve discussed the requirements for setting up the app to find static files and find and compile Razor views in the context of a test.
    • Set up the content root in the tests to the application’s root folder.
    • Ensure the test project references all the assemblies in the application.
    • Copy the application’s deps file to the bin folder of the test project.
    • Disable shadow copying in your testing framework of choice.
  • We’ve shown how to write a functional test in-memory using TestServer and the same configuration your app uses when running on a real server in Production.

This post has covered the basics for getting you up and running with in-memory tests. There are several areas that are left as an exercise to the reader:

  • Override services for testing.
  • Mock other dependencies for testing (like the database)
  • Seed testing data in your application.

The source code of the completed project is available here: https://github.com/aspnet/samples/tree/master/samples/aspnetcore/mvc/testing

Happy testing!

1 comment

Discussion is closed. Login to edit/delete existing comments.

  • Paul Freedman 0

    Nice post, but there seem to be some code snippets missing.

Feedback usabilla icon