Use ServiceCollection in Unit Tests with .NET

A popular unit test - and also a necessary test - is the correct registration of interfaces and their implementation with dependency injection. And a common mistake is that the associated IServiceCollection interface is used for mocks that lead to faulty tests.

The problem

Given the following code, in which an interface and an implementation are registered in a .NET application.

 1using Microsoft.Extensions.DependencyInjection;
 2using System;
 3
 4public interface IMyService
 5{
 6    void DoSomething();
 7}
 8
 9public class MyService : IMyService
10{
11    public void DoSomething()
12    {
13        Console.WriteLine("Doing something...");
14    }
15}
16
17public class Startup
18{
19    public void ConfigureServices(IServiceCollection services)
20    {
21        // Registering the singleton
22        services.AddSingleton<IMyService, MyService>();
23    }
24}

An obvious test would be to check that AddSingleton of the interface is called.

 1public class StartupTests
 2{
 3    [Fact]
 4    public void ConfigureServices_ShouldRegisterIMyServiceAsSingleton()
 5    {
 6        // Arrange
 7        IServiceCollection services = Substitute.For<IServiceCollection>();
 8        Startup startup = new Startup();
 9
10        // Act
11        startup.ConfigureServices(services);
12
13        // Assert
14        services.Received().AddSingleton<IMyService, MyService>();
15    }
16}

The problem is that AddSingleton is a simple extension method that itself cannot be easily mocked and thus used for mocked tests.

 1// Licensed to the .NET Foundation under one or more agreements.
 2// The .NET Foundation licenses this file to you under the MIT license.
 3
 4// Microsoft.Extensions.DependencyInjection.Abstractions.dll
 5
 6public static IServiceCollection AddSingleton<TService, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TImplementation>(this IServiceCollection services)
 7    where TService : class
 8    where TImplementation : class, TService
 9{
10    ThrowHelper.ThrowIfNull(services);
11
12    return services.AddSingleton(typeof(TService), typeof(TImplementation));
13}

A corresponding test fails with the following error message:

Message:  NSubstitute.Exceptions.ReceivedCallsException : Expected to receive a call matching: Add(ServiceType: IMyService Lifetime: Singleton ImplementationType: MyService) Actually received no matching calls. Received 1 non-matching call (non-matching arguments indicated with ‘*’ characters): Add(ServiceType: IMyService Lifetime: Singleton ImplementationType: MyService)

Use of ServiceCollection

The generally recommended solution in the community is not to use the interface IServiceCollection for testing, but the implementation ServiceCollection.

 1// Arrange
 2ServiceCollection services = new ServiceCollection();
 3Startup startup = new Startup();
 4
 5// Act
 6startup.ConfigureServices(services);
 7
 8// Assert
 9ServiceDescriptor? serviceDescriptor = services
10    .Where(
11        serviceDescriptor => serviceDescriptor.ServiceType == typeof(IMyService)
12        && serviceDescriptor.ImplementationType == typeof(MyService)
13        && serviceDescriptor.Lifetime == ServiceLifetime.Singleton)
14    .SingleOrDefault();
15
16Assert.NotNull(serviceDescriptor);

The enormous advantage is that I am not testing a mock, but the real implementation of ServiceCollection, which is also used at runtime. Furthermore, asserting ServiceCollection in more complex tests is much easier.

To make it even easier, you can build simple test extensions to reduce the test effort per test:

 1/// <summary>
 2/// Provides extension methods for verifying the registration of services in a <see cref="ServiceCollection"/>.
 3/// </summary>
 4public static class ServiceCollectionExtensions
 5{
 6    /// <summary>
 7    /// Verifies that a specific service and its implementation are registered in the <see cref="ServiceCollection"/> 
 8    /// with the <see cref="ServiceLifetime.Singleton"/> lifetime the specified number of times.
 9    /// </summary>
10    /// <typeparam name="TService">The service type to verify.</typeparam>
11    /// <typeparam name="TImplementation">The implementation type of the service to verify.</typeparam>
12    /// <param name="collection">The <see cref="ServiceCollection"/> to check.</param>
13    /// <param name="times">The expected number of times the service and its implementation are registered.</param>
14    public static void VerifyAddSingleton<TService, TImplementation>(this ServiceCollection collection, int times = 1)
15        where TImplementation : class, TService
16    {
17        VerifyAdd<TService, TImplementation>(collection, ServiceLifetime.Singleton, times);
18    }
19
20    /// <summary>
21    /// Verifies that a specific service and its implementation are registered in the <see cref="ServiceCollection"/> 
22    /// with the specified lifetime the specified number of times.
23    /// </summary>
24    /// <typeparam name="TService">The service type to verify.</typeparam>
25    /// <typeparam name="TImplementation">The implementation type of the service to verify.</typeparam>
26    /// <param name="collection">The <see cref="ServiceCollection"/> to check.</param>
27    /// <param name="lifetime">The expected lifetime of the service.</param>
28    /// <param name="times">The expected number of times the service and its implementation are registered.</param>
29    public static void VerifyAdd<TService, TImplementation>(this ServiceCollection collection, ServiceLifetime lifetime, int times)
30        where TImplementation : class, TService
31    {
32        // Arrange
33        Type serviceType = typeof(TService);
34        Type implementationType = typeof(TImplementation);
35
36        // Act
37        List<ServiceDescriptor> items = collection
38            .Where(
39                serviceDescriptor => serviceDescriptor.ServiceType == typeof(TService)
40                && serviceDescriptor.ImplementationType == typeof(TImplementation)
41                && serviceDescriptor.Lifetime == lifetime)
42            .ToList();
43
44        // Assert
45        Assert.Equal(times, items.Count);
46    }
47
48    /// <summary>
49    /// Verifies that a specific service is registered in the <see cref="ServiceCollection"/> 
50    /// with the specified lifetime the specified number of times.
51    /// </summary>
52    /// <typeparam name="TService">The service type to verify.</typeparam>
53    /// <param name="collection">The <see cref="ServiceCollection"/> to check.</param>
54    /// <param name="lifetime">The expected lifetime of the service.</param>
55    /// <param name="times">The expected number of times the service is registered.</param>
56    public static void VerifyAdd<TService>(this ServiceCollection collection, ServiceLifetime lifetime, int times)
57    {
58        // Arrange
59        Type serviceType = typeof(TService);
60
61        // Act
62        List<ServiceDescriptor> items = collection
63            .Where(
64                serviceDescriptor => serviceDescriptor.ServiceType == typeof(TService)
65                && serviceDescriptor.Lifetime == lifetime)
66            .ToList();
67
68        // Assert
69        Assert.Equal(times, items.Count);
70    }
71}

The test looks correspondingly slimmer:

1// Arrange
2ServiceCollection services = new ServiceCollection();
3Startup startup = new Startup();
4
5// Act
6startup.ConfigureServices(services);
7
8// Assert
9services.VerifyAddSingleton<IMyService, MyService>();

Using NSubstitute

A potential further solution with NSubstitute is to mock the base method Add and check whether it has been called with the appropriate parameters.

 1// Arrange
 2IServiceCollection services = Substitute.For<IServiceCollection>();
 3Startup startup = new Startup();
 4
 5// Act
 6startup.ConfigureServices(services);
 7
 8// Assert
 9services.Received().Add(Arg.Is<ServiceDescriptor>(descriptor =>
10    descriptor.ServiceType == typeof(IMyService) &&
11    descriptor.ImplementationType == typeof(MyService) &&
12    descriptor.Lifetime == ServiceLifetime.Singleton));

Conclusion

Simpler and better quality tests can be achieved when verifying dependency injection with ServiceCollection.


Comments

Twitter Facebook LinkedIn WhatsApp