When Microsoft built ASP.NET Core from the ground up, they fixed and improved so many things that I cannot enumerate them all here, including testability.Nowadays, there are two ways to structure a .NET program:
- The classic ASP.NET Core Program and the Startup classes. This model might be found in existing projects (created before .NET 6).
- The minimal hosting model introduced in .NET 6. This may look familiar to you if you know Node.js, as this model encourages you to write the start-up code in the Program.cs file by leveraging top-level statements. You will most likely find this model in new projects (created after the release of .NET 6).
No matter how you write your program, that’s the place to define how the application’s composition and how it boots. Moreover, we can leverage the same testing tools more or less seamlessly.In the case of a web application, the scope of our integration tests is often to call the endpoint of a controller over HTTP and assert the response. Luckily, in .NET Core 2.1, the .NET team added the WebApplicationFactory<TEntry> class to make the integration testing of web applications easier. With that class, we can boot up an ASP.NET Core application in memory and query it using the supplied HttpClient in a few lines of code. The test classes also provide extension points to configure the server, such as replacing implementations with mocks, stubs, or other test-specific elements.Let’s start by booting up a classic web application test.
Classic web application
In a classic ASP.NET Core application, the TEntry generic parameter of the WebApplicationFactory<TEntry> class is usually the Startup or Program class of your project under test.
The test cases are in the Automated Testing solution under the MyApp.IntegrationTests project.
Let’s start by looking at the test code structure before breaking it down:
namespace MyApp.IntegrationTests.Controllers;
public class ValuesControllerTest : IClassFixture<WebApplicationFactory<Startup>>
{
private readonly HttpClient _httpClient;
public ValuesControllerTest(
WebApplicationFactory<Startup> webApplicationFactory)
{
_httpClient = webApplicationFactory.CreateClient();
}
public class Get : ValuesControllerTest
{
public Get(WebApplicationFactory<Startup> webApplicationFactory)
: base(webApplicationFactory) { }
[Fact]
public async Task Should_respond_a_status_200_OK()
{
// Omitted Test Case 1
}
[Fact]
public async Task Should_respond_the_expected_strings()
{
// Omitted Test Case 2
}
}
}
The first piece of the preceding code that is relevant to us is how we get an instance of the WebApplicationFactory<Startup> class. We inject a WebApplicationFactory<Startup> object into the constructor by implementing the IClassFixture<T> interface (a xUnit feature). We can also use the factory to configure the test server, but we don’t need to here, so we can only keep a reference on the HttpClient, preconfigured to connect to the in-memory test server.Then, we may have noticed we have the nested Get class that inherits the ValuesControllerTest class. The Get class contains the test cases. By inheriting the ValuesControllerTest class, we can leverage the _httpClient field from the test cases we are about to see.In the first test case, we use HttpClient to query the http://localhost/api/values URI, accessible through the in-memory server. Then, we assert that the status code of the HTTP response was a success (200 OK):
[Fact]
public async Task Should_respond_a_status_200_OK()
{
// Act
var result = await _httpClient
.GetAsync(“/api/values”);
// Assert
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
}
The second test case also sends an HTTP request to the in-memory server but deserializes the body’s content as a string[] to ensure the values are the same as expected instead of validating the status code:
[Fact]
public async Task Should_respond_the_expected_strings()
{
// Act
var result = await _httpClient
.GetFromJsonAsync<string[]>(“/api/values”);
// Assert
Assert.Collection(result,
x => Assert.Equal(“value1”, x),
x => Assert.Equal(“value2”, x)
);
}
As you may have noticed from the test cases, the WebApplicationFactory preconfigured the BaseAddress property for us, so we don’t need to prefix our requests with http://localhost.
When running those tests, an in-memory web server starts. Then, HTTP requests are sent to that server, testing the complete application. The tests are simple in this case, but you can create more complex test cases in more complex programs.Next, we explore how to do the same for minimal APIs.