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 onTestServer
.
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. The test fixture sets up an HttpClient
for you that allows you to invoke your app in-memory.
using Xunit;
namespace TestingMvc.Tests
{
public class TestingMvcFunctionalTests : IClassFixture<WebApplicationTestFixture<Startup>>
{
public MyApplicationFunctionalTests(WebApplicationTestFixture<Startup> fixture)
{
Client = fixture.Client;
}
public HttpClient Client { get; }
[Fact]
public async Task GetHomePage()
{
// Arrange & Act
var response = await Client.GetAsync("/");
// Assert
Assert.Equal(HttpStatusCodes.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.Client;
}
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 reconfigure the HttpClient
to use an HTTPS base address instead.
public TestingMvcFunctionalTests(TestingMvcTestFixture<Startup> fixture)
{
Client = fixture.Client;
Client.BaseAddress = 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.