Unit testing IServiceCollection registrations in .NET enables us to test our application’s IServiceCollection to confirm that the setup of dependency injection is correct, allowing us to catch errors early in development. E.g. if we fail to register a service or register a service incorrectly with the wrong lifetime, we would be able to detect such issues. This would help raise problems such as missing or misconfigured services early, rather than at runtime, giving our application stability. It also offers developers a technique that ensures the correct dependencies are injected into an application as intended, enabling maintainable and reliable code.

To download the source code for this article, you can visit our GitHub repository.

For a comprehensive understanding of Dependency Injection in .NET, we recommend reading our article on Dependency Injection in ASP.NET Core.

Let’s begin!

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

Why Should We Test IServiceCollection Registrations?

When working on large projects, the code for setting up services for dependency injection can get quite extensive. If we forget to register a new dependency, our application will fail when it tries to create a class that needs that dependency. 

Testing our IServiceCollection registrations allow us to detect issues early. It promotes best coding practices and ensures correct service configuration.

Additionally, it improves code maintainability. These benefits ultimately contribute to the overall quality and stability of our applications.

Let’s now register some services for our test.

IServiceCollection Registrations for Different Lifetimes

In .NET, service lifetimes determine a service instance’s scope and duration. These service lifetimes include Singleton (one instance per application), Scoped (one instance per request), and Transient (a new instance each time requested). We will implement testing of these three service lifetimes as we progress.

To know more about the different service lifetimes, check out our article Dependency Injection Lifetimes in ASP.NET Core

Now, let’s create three services for the three different lifetimes. Although the implementations for the lifetimes are similar, we will demonstrate all three for this article.

Let’s create the first service that we’ll use as a singleton:

public interface IPetService
{
    void PrintName(string name);
}

public class PetService : IPetService
{
    public void PrintName(string name)
    {
        Console.WriteLine("The name of the pet is {0}", name);
    }
}

Next, let’s create a service for a scoped instance:

public interface IMarineAnimalsService
{
    void PrintName(string name);
}

public class MarineAnimalsService : IMarineAnimalsService
{
    public void PrintName(string name)
    {
        Console.WriteLine("Marine animals like {0} live in the ocean", name);
    }
}

Lastly, we create a service for our transient instance:

public interface IWildAnimalService
{
    void PrintName(string name);
}

public class WildAnimalService : IWildAnimalService
{
    public void PrintName(string name)
    {
        Console.WriteLine("Wild animals like {0} are not domesticated", name);
    }
}

Let’s note that the best way to test service registrations is by creating an extension class. Creating an extension method for the registration of all dependencies in one place makes it easier to test. We will be able to write unit tests for the IServiceCollection class in an immaculate way.

Let’s now create our extension method for registering dependencies:

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddDependencies(this IServiceCollection services)
    {
        services.AddSingleton<IPetService, PetService>();
        services.AddTransient<IWildAnimalService, WildAnimalService>();
        services.AddScoped<IMarineAnimalsService, MarineAnimalsService>();

        return services;
    }
}

We create the ServiceCollectionExtensions class and then add the AddDependencies() method to handle the registrations.

Adding Unit Tests for IServiceCollection Registrations

Let’s assume we forgot to register our services. We will get an error when we run our application:

Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
   at Program.<Main>$(String[] args)

Writing tests before writing the actual code is advisable because these unit tests detect bugs earlier in the development phase. This process is called Test-Driven Development (TDD).

Before we start writing tests, we’ll add a new XUnit project to our solution.

To use a mocking framework like NSubstitute for unit testing the AddDependencies() method, we typically mock the service dependencies rather than the IServiceCollection itself. However, to verify the service registration, we don’t need to use NSubstitute. Instead, we will directly verify the service collection.

Now, let’s implement our unit test to test our services:

public class ServiceCollectionTest
{
    private readonly IServiceCollection _serviceCollection = new ServiceCollection();
    private readonly IServiceProvider _serviceProvider;

    public ServiceCollectionTest()
    {
        _serviceCollection.AddDependencies();
        _serviceProvider = _serviceCollection.BuildServiceProvider();
    }
}

We initialize a ServiceCollection class, adding dependencies using AddDependencies() method, and then building the ServiceProvider.

Now, let’s add tests for the three lifetimes. Each test will check that a specific service is registered with the correct lifetime:

[Theory]
[InlineData(typeof(IPetService), typeof(PetService), ServiceLifetime.Singleton)]
[InlineData(typeof(IWildAnimalService), typeof(WildAnimalService), ServiceLifetime.Transient)]
[InlineData(typeof(IMarineAnimalsService), typeof(MarineAnimalsService), ServiceLifetime.Scoped)]
public void GivenDependenciesExists_WhenServiceIsRegistered_ThenServiceIsRegistered
(Type interfaceType, Type classType, ServiceLifetime serviceLifetime)
{
    var serviceType = _serviceProvider.GetService(interfaceType);

    Assert.NotNull(serviceType);

    var serviceDescriptor = _serviceCollection.SingleOrDefault(
        d => d.ServiceType == interfaceType &&
             d.ImplementationType == classType &&
             d.Lifetime == serviceLifetime);

    Assert.NotNull(serviceDescriptor);
}

We add a generic unit test GivenDependenciesExists_WhenServiceIsRegistered_ThenServiceIsRegistered using the [Theory] and [InlineData] attribute to verify the service registrations.

By using this approach we create a flexible and reusable unit test that can verify the registration of various services with different lifetimes.

We use the [InlineData] attribute to provide different sets of parameters for the test method, adding IPetService, IWildAnimalService and IMarineAnimalsService as test parameters.

Let’s run tests with dotnet test and verify all the tests pass:

Passed! - Failed: 0, Passed: 3, Skipped: 0, Total: 3, Duration: 5 ms - Tests.dll (net8.0)

By following these steps, we effectively test our AddDependencies() method to ensure we correctly register services in the IServiceCollection.

Conclusion

In this article, we learned how to test IServiceCollection Registrations in .NET. Unit testing service registration in .NET is a vital technique to ensure that the IServiceCollection dependency injection setup is correct. Also, we discussed the importance of unit testing service registrations by creating an extension method for service registration, making it easier to test.  By following these practices, we can ensure that our dependency injection setup in .NET is robust and reliable, leading to a more stable and maintainable application.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!