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.