Skip to content
  • English Deutsch

benjamin-abt.com

Performance is a feature | Cloud - AI - Azure - .NET - Sustainability

  • Home
  • Blog
  • Portfolio
    • Overview
    • Workshops & Training
    • Publications
  • Contact
  • Home
  • Blog

Use ServiceCollection in Unit Tests with .NET

Use ServiceCollection in Unit Tests with .NET
October 10, 2024 5 min read BEN ABT
Csharp .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.

C#
 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.

C#
 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.

C#
 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.

C#
 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:

C#
 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:

C#
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.

C#
 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.

Related articles

Containerizing .NET in 2026 Without a Dockerfile

Oct 20, 2026 - 11 min read

Containerizing .NET in 2026 Without a Dockerfile

Most .NET Dockerfiles repeat the same five steps: restore, publish, select the matching Microsoft runtime image, copy the output and start …

Problem Details in ASP.NET Core and .NET 11

Oct 5, 2026 - 12 min read

Problem Details in ASP.NET Core and .NET 11

builder.Services.AddProblemDetails() registers infrastructure. It does not create an error contract. An ASP.NET Core request can fail during …

Resilience in .NET with Microsoft.Extensions.Resilience

Sep 21, 2026 - 11 min read

Resilience in .NET with Microsoft.Extensions.Resilience

Retries are the most visible resilience strategy and the easiest one to misuse. Repeating a failing call can recover from a short network …


BEN ABT

BEN ABT

Ben is Chief Engineering Officer and Principal Software Engineer at Medialesson.de, with a strong focus on .NET and Microsoft Azure. In his professional role, he designs and builds highly scalable platforms for hybrid and cloud-based scenarios. He also advises companies and C-levels on migrating to the sovereign cloud.

He runs myCSharp.de, the largest and most active German-speaking C# community forum, actively contributes to both his own and external open-source projects and regularly shares his knowledge through his blog, at community events, conferences and as a writer for technical magazines.

Let's Work Together

Looking for an experienced Platform Architect or Engineer for your next project? Whether it's cloud migration, platform modernization or building new solutions from scratch - I'm here to help you succeed.

New Platforms

Modernization

Training & Consulting

Contact Me
X Facebook LinkedIn WhatsApp
Back Next Blog Post

Open for work!

Contact Me
BEN ABT BEN ABT Software Engineer · Microsoft MVP Passionate about .NET, Azure, performance optimization and sustainable software engineering.

Medialesson GmbH Logo

Microsoft MVP Logo

Recent posts

View all
Stop Using nginx as Your Default Static File Server in Docker

Stop Using nginx as Your Default Static File Server in …

Oct 24, 2026
Containerizing .NET in 2026 Without a Dockerfile

Containerizing .NET in 2026 Without a Dockerfile

Oct 20, 2026
Problem Details in ASP.NET Core and .NET 11

Problem Details in ASP.NET Core and .NET 11

Oct 5, 2026
Resilience in .NET with Microsoft.Extensions.Resilience

Resilience in .NET with Microsoft.Extensions.Resilience

Sep 21, 2026
What Matters in ASP.NET Core 11

What Matters in ASP.NET Core 11

Sep 14, 2026

Tags

.NET (192) Csharp (96) ASP.NET Core (48) Azure (41) DevOps (17) Docker (14) Microsoft (14) EF Core (11) Microsoft MVP (11) AI (10) GitHub (10) GitHub Copilot (10) ASP.NET (9) Security (9) Synology (9) Architecture (8) Raspberry (8) Azure Functions (7) Cloudflare (7) Bicep (6)
benjamin-abt.com

About BEN

Benjamin Abt is a Software Engineer, Platform Architect and Microsoft MVP specializing in .NET, Azure, cloud architecture, performance optimization and sustainable software development.

Connect

  • Follow me on GitHub
  • Follow me on LinkedIn
  • Follow me on Mastodon
  • Microsoft MVP profile
  • Follow SchwabenCode on GitHub
  • Follow me on X

Quick Links

  • Portfolio
  • Blog
  • Workshops & Training
  • Publications
  • Get in Touch
  • Sitemap
Open for Work

© Copyright Benjamin Abt & SchwabenCode | 2011 - 2026

Content licensed under CC BY 4.0 unless otherwise noted.

  • Impressum
  • Privacy
  • Contact