With integration testing, we need to run an instance of our application to properly test the API. Furthermore, we want to be able to override configuration, such as our SQL database, so that we control the environment we test against and don’t pollute our actual database with test data.
Within the .NET ecosystem, we can achieve this with the WebApplicationFactory class from the Microsoft.AspNetCore.Mvc.Testing NuGet package, so let’s start with installing that in our test project:
dotnet add package Microsoft.AspNetCore.Mvc.Testing -v 9.0.0
With this installed, let’s understand the WebApplicationFactory class better.
Bootstrap Application With WebApplicationFactory
The WebApplicationFactory class allows us to bootstrap our application by taking an instance of the Program class, with methods to override specific parts of the startup process. To allow our test project to reference the Program class correctly from our Order.Service project, we need to add a line to the bottom of our Program class:
app.Run();
public partial class Program { }
Now we’re ready to bootstrap our application. Back in our test project, let’s create a new class:
using Microsoft.AspNetCore.Mvc.Testing;
namespace Order.Tests;
public class OrderWebApplicationFactory : WebApplicationFactory<Program>
{
}
We start by including a reference to the Microsoft.AspNetCore.Mvc.Testing namespace, which gives us access to the WebApplicationFactory class. Now, we have access to override a bunch of methods for different stages of our application startup. So what do we need to override? Our Order microservice has a database connection to the Order database running in Docker. But we don’t want to use this database for test purposes, so let’s override this. We can still use Docker to create our test databases so we can replicate our actual implementation, but we want to use a different database.
In our test project, let’s add a new appsettings.Tests.json file:
{
"ConnectionStrings": {
"Default": "Server=localhost,1433;Database=Order_Tests;User Id=sa;Password=micR0S3rvice$;TrustServerCertificate=True"
},
"RabbitMq": {
"HostName": "localhost"
}
}
Although this looks very similar to our appsettings.json file in our Order microservice, it differs when we define the database name, Order_Tests. If we take a look at the EntityFrameworkExtensions class:
public static class EntityFrameworkExtensions
{
public static void AddSqlServerDatastore(this IServiceCollection services,
IConfigurationManager configuration)
{
services.AddDbContext<OrderContext>(options =>
options.UseSqlServer(configuration.GetConnectionString("Default")));
services.AddScoped<IOrderStore, OrderContext>();
}
}
We use the GetConnectionString() method, looking for the Default key. With the WebApplicationFactory class, we can provide our test configuration file as an override, so let’s do that:
protected override IHost CreateHost(IHostBuilder builder)
{
builder.ConfigureHostConfiguration(configurationBuilder =>
{
configurationBuilder.AddConfiguration(new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.Tests.json")
.Build());
});
return base.CreateHost(builder);
}
With a using statement added for Microsoft.Extensions.Hosting and Microsoft.Extensions.Configuration, we override the CreateHost() method, and call the ConfigureHostConfiguration() method. Instead of using the appsettings.json file from our Order microservice, we use the appsettings.Tests.json file from our test project. We have a further change to ensure our appsettings.Tests.json file gets copied to the output directory when running the tests. In our Order.Tests.csproj file, let’s add a new <ItemGroup> section:
<ItemGroup> <None Update="appsettings.Tests.json"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </None> </ItemGroup>
We specify the name of our JSON file and set the CopyToOutputDirectory element to Always. With this, we can move on to our next step.
Scaffold Test Database
We need a way to scaffold our test database and apply migrations so the correct database schema is present. Before we can reference our OrderContext class, which is marked as internal, we need to add a new section to our Order.Service.csproj file:
<ItemGroup> <InternalsVisibleTo Include="Order.Tests" /> </ItemGroup>
With this, we can override another method from the WebApplicationFactory class:
public class OrderWebApplicationFactory : WebApplicationFactory<Program>
{
private OrderContext? _orderContext;
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureTestServices(ApplyMigrations);
}
private void ApplyMigrations(IServiceCollection services)
{
var serviceProvider = services.BuildServiceProvider();
var scope = serviceProvider.CreateScope();
_orderContext = scope.ServiceProvider.GetRequiredService<OrderContext>();
_orderContext.Database.Migrate();
}
}
First, we include using statements for Microsoft.AspNetCore.Hosting, Microsoft.AspNetCore.TestHost, Microsoft.EntityFrameworkCore, Microsoft.Extensions.DependencyInjection and Order.Service.Infrastructure.Data.EntityFramework. With this, we override the ConfigureWebHost() method, and call the ConfigureTestServices() method. This method can be used to manipulate the service collection, adding or removing registrations as we need to correctly configure our test environment. We don’t need to add or remove any services, but we do need to apply database migrations, so we define a private method ApplyMigrations().
In this method, we build the service collection to retrieve an instance of the ServiceProvider class. With this, we call the CreateScope() method and retrieve an instance of our OrderContext class, assigning it to a private field and calling the Migrate() method to create the database and apply any migrations. This is great for setting up our database, but we need to remove it whenever we finish a test so we have a clean environment each time. For this, we can utilize the IAsyncLifetime interface from xUnit, which works similarly to the IDisposable interface from the .NET base library.
So, let’s inherit this interface and implement the required methods:
public class OrderWebApplicationFactory : WebApplicationFactory<Program>, IAsyncLifetime
{
private OrderContext? _orderContext;
public Task InitializeAsync() => Task.CompletedTask;
public new Task DisposeAsync()
{
if (_orderContext is not null)
{
return _orderContext.Database.EnsureDeletedAsync();
}
return Task.CompletedTask;
}
}
We have two methods to implement. First, we don’t need to do any initialization in the InitializeAsync() method, so we return Task.CompletedTask. In the DisposeAsync() method, we check if our private field is not null, calling the EnsureDeletedAsync() method and returning a CompletedTask once again. It is worth noting we add the new keyword in the method declaration, otherwise, we hide the base class implementation. With this, our database is ready to go. We’re ready to get started with our tests.