Unit Testing Dependency Injection Container Registrations in ASP.NET
Have you ever thought of unit testing the code used to register services in your ASP.NET application? It’s not a bad idea. Let’s see how.
Service Registration
In the default ASP.NET templates, pre .NET 6, services were registered in the ConfigureServices
method of the Startup
class. .NET 6 introduced minimal API templates which did away with the Startup
class. Service registrations are sone within the Program
class. In either case, it can make a lot of sense to
create separate methods to register the services for your application.
For example, let’s assume your application has a service called ITimeProvider
that is registered as a singleton. One option would be to create an extension method to perform the registration:
public static class DomainServiceCollectionExtensions
{
public static IServiceCollection AddDomainServices(this IServiceCollection services, IConfiguration configuration)
{
services.AddSingleton<ITimeProvider, TimeProvider>();
// Other services registered here
return services;
}
}
Adding a Unit Test
On more than one occasion, I have completed a pull request, with all my unit tests passing, only to find that I had an incorrect (or missing) service registration. I want to get into the habit of creating unit tests to ensure the services are registered as I would expect. The method needs two input parameters; an implementation of the IServiceCollection
interface and an implementation of the IConfiguration
interface.
The ServiceCollection
class is the implementation of the IServiceCollection
we can use for our test (and is what the ASP.NET framework uses). And you can use a ConfigurationBuilder
to create an instance of the IConfiguration
interface:
var services = new ServiceCollection();
var configuration = new ConfigurationBuilder().Build();
Now we can write a test for the registration of the ITimeProvider
service:
public void ITimeProvider_Registered_As_Singleton()
{
// Arrange
var services = new ServiceCollection();
var configuration = new ConfigurationBuilder().Build();
// Act
services.AddDomainServices(configuration);
// Assert
var _ = Assert.Single(services, x =>
x.ServiceType == typeof(ITimeProvider) &&
x.Lifetime == ServiceLifetime.Singleton);
}
Testing Conditional Registrations
Some registrations are only made under certain conditions. A typical example is the use of feature toggles.
Let us assume that the ASP.NET application has a hosted service that runs in the background. It should be registered
if the configuration includes the following feature toggle in the appsettings.json
:
{
"EnableBackgroundService": true
}
Here is the updated registration logic:
public static class DomainServiceCollectionExtensions
{
public static IServiceCollection AddDomainServices(this IServiceCollection services, IConfiguration configuration)
{
services.AddSingleton<ITimeProvider, TimeProvider>();
var enableBackgroundService = configuration.GetValue<bool>("EnableBackgroundService");
if (enableBackgroundService)
{
services.AddHostedService<MyBackgroundService>();
}
// Other services registered here
return services;
}
}
We can arrange our test using an in-memory configuration source, rather than using a json file:
public void MyBackgroundService_Registered_As_Singleton()
{
// Arrange
var services = new ServiceCollection();
var initialData = new Dictionary<string, string>()
{
{"EnableBackgroundService", "true"}
};
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(initialData)
.Build();
// Act
services.AddDomainServices(configuration);
// Assert
var _ = Assert.Single(services, x =>
x.ServiceType == typeof(MyBackgroundService) &&
x.Lifetime == ServiceLifetime.Singleton);
}
And don’t forget to include the corresponding test if the service should not be registered:
public void MyBackgroundService_Not_Registered()
{
// Arrange
var services = new ServiceCollection();
var initialData = new Dictionary<string, string>()
{
{"EnableBackgroundService", "false"}
};
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(initialData)
.Build();
// Act
services.AddDomainServices(configuration);
// Assert
Assert.Empty(services.Where(x => x.ServiceType == typeof(MyBackgroundService)));
}
Summary
I think it wise to add unit tests for the services being registered with an application’s Dependency Injection Container. It can reveal some pretty interesting bugs. The basic registration scenarios are straightforward to test. As the registrations become more complex, the tests become more complex as well. But it is not impossible, especially if the registrations are refactored into separate classes.