Testing an ASP.NET Core web app
In this guide you will learn how to
Use Testcontainers to spin up test dependencies
Replace SQLite with Microsoft SQL Server
Prerequisites
Testcontainers requires a Docker-API compatible container runtime.
This guide is built on top of Microsoft’s guide Integration tests in ASP.NET Core. You will find Microsoft’s code sample here. Anything we learn in this guide is done on top of the referenced sources.
What we are going to achieve in this guide
The following section explains how we can replace SQLite with a database provider that is used in production. This will help us to increase our confidence in our tests and to enable testing in an environment that closely resembles reality.
Getting Started
Change to the tests/RazorPagesProject.Tests
directory and install the Microsoft.EntityFrameworkCore.SqlServer
and Testcontainers.MsSql
NuGet dependency.
cd AspNetCore.Docs.Samples/test/integration-tests/IntegrationTestsSample/tests/RazorPagesProject.Tests
dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 7.0.0
dotnet add package Testcontainers.MsSql --version 3.0.0
Testcontainers for .NET offers a range of modules that follow best practice configurations. |
Now that all dependencies have been set up, we can proceed to add another test class to the project. First, we will create a MsSqlTests
class that will be responsible for configuring, creating, and starting the dependent Microsoft SQL Server container. The MsSqlTests
class will contain a nested IndexPageTests
class that will run the tests. This will allow us to access the private container field and follow a neat hierarchy in the test explorer.
public sealed class MsSqlTests : IAsyncLifetime
{
private readonly MsSqlContainer _msSqlContainer = new MsSqlBuilder().Build();
public Task InitializeAsync()
{
return _msSqlContainer.StartAsync();
}
public Task DisposeAsync()
{
return _msSqlContainer.DisposeAsync().AsTask();
}
public sealed class IndexPageTests : IClassFixture<MsSqlTests>, IDisposable
{
private readonly WebApplicationFactory<Program> _webApplicationFactory;
private readonly HttpClient _httpClient;
public IndexPageTests(MsSqlTests fixture)
{
var clientOptions = new WebApplicationFactoryClientOptions();
clientOptions.AllowAutoRedirect = false;
_webApplicationFactory = new CustomWebApplicationFactory(fixture);
_httpClient = _webApplicationFactory.CreateClient(clientOptions);
}
public void Dispose()
{
_webApplicationFactory.Dispose();
}
private sealed class CustomWebApplicationFactory : WebApplicationFactory<Program>
{
private readonly string _connectionString;
public CustomWebApplicationFactory(MsSqlTests fixture)
{
_connectionString = fixture._msSqlContainer.GetConnectionString();
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
services.Remove(services.SingleOrDefault(service => typeof(DbContextOptions<ApplicationDbContext>) == service.ServiceType));
services.Remove(services.SingleOrDefault(service => typeof(DbConnection) == service.ServiceType));
services.AddDbContext<ApplicationDbContext>((_, option) => option.UseSqlServer(_connectionString));
});
}
}
}
}
The Microsoft SQL Server Docker image is not compatible with ARM devices, such as Macs with Apple Silicon. Instead, you can use the SqlEdge module or Testcontainers Cloud. |
Testcontainers modules are pre-configured and follow best practices acknowledged by many developers. Usually, you do not need to worry about configuring them yourself. All you need to do is to create a new container instance using new MsSqlBuilder().Build()
. However, in some cases, it may be necessary to use your own configuration. In such cases, Testcontainers offers the generic builder ContainerBuilder()
.
xUnit.net calls IAsyncLifetime.InitializeAsync
immediately after the class has been created. Our test uses this mechanism to start the Microsoft SQL Server instance before any test run.
The IndexPageTests
class creates a custom instance of WebApplicationFactory<TEntryPoint>
. Instead of adding a database context that relies on SQLite, we simply pass our Microsoft SQL Server connection string to UseSqlServer(string)
to add a new database context. With Testcontainers, you can even shift this entire configuration to the web application entry point class and start dependent services together with your application.
Now that the test class is ready, we can move the original tests to it. For example, copy the following test to our new IndexPageTests
class and run the test against a Microsoft SQL Server instance:
[Fact]
public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot()
{
// Arrange
var defaultPage = await _httpClient.GetAsync("/")
.ConfigureAwait(false);
var document = await HtmlHelpers.GetDocumentAsync(defaultPage)
.ConfigureAwait(false);
// Act
var form = (IHtmlFormElement)document.QuerySelector("form[id='messages']");
var submitButton = (IHtmlButtonElement)document.QuerySelector("button[id='deleteAllBtn']");
var response = await _httpClient.SendAsync(form, submitButton)
.ConfigureAwait(false);
// Assert
Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.Equal("/", response.Headers.Location.OriginalString);
}
Please note that the first test run might take a few seconds longer since we need to pull the required image first. |
Summary
By replacing SQLite with a database provider used in production, developers can further increase their confidence in their tests. The MsSqlTests
class uses Testcontainers to configure, create and start a Microsoft SQL Server container, allowing the IndexPageTests
class to test the application against the real database. This approach allows developers to test their application in a production-like environment and helps to identify issues early in the development cycle.
To learn more about Testcontainers visit: https://www.testcontainers.com