DEV Community

Daan Acohen
Daan Acohen

Posted on

TUnit + Dependency Injection in .NET Tests: One Registration, Zero Boilerplate

Introduction

If your production app uses dependency injection, your tests should too.

With TUnit, you can inject dependencies directly into your test class in a clean, first-class way using DependencyInjectionDataSourceAttribute<TScope>. TUnit’s own docs explicitly recommend this approach for user-provided services.

👉 Example repository used in this article:
https://github.com/ConnectingApps/TUnitPublicDemo


The real problem

Many .NET teams do this well in production:

  • register services in IServiceCollection
  • resolve dependencies via constructor injection

But tests often drift into ad-hoc setup:

  • repeated new calls
  • test-specific composition roots
  • fixture patterns that diverge from runtime wiring

That drift increases maintenance cost and weakens refactor safety.


Why TUnit is interesting here

TUnit provides a dedicated DI data source pattern for test dependencies (DependencyInjectionDataSourceAttribute<TScope>), instead of hiding everything behind framework internals.

Also useful context: TUnit positions itself as a modern .NET testing framework with source generation and fast execution characteristics


Main pattern (from the repo)

The repository demonstrates one simple architectural rule:

Use the same service registration extension in production and in tests.

1) Put registrations in one extension method

public static class DependencySetup
{
    public static IServiceCollection AddGravity(this IServiceCollection services)
    {
        services.AddSingleton<IGravityConfiguration, GravityConfiguration>();
        services.AddScoped<IGravityCalculator, GravityCalculator>();
        return services;
    }
}
Enter fullscreen mode Exit fullscreen mode

2) Use it in production (Program.cs)

builder.Services.AddGravity();
Enter fullscreen mode Exit fullscreen mode

3) Use it in tests via a TUnit DI attribute

Create a custom attribute inheriting from DependencyInjectionDataSourceAttribute<TScope>, build your ServiceProvider, and resolve constructor parameters from there.

4) Inject dependencies directly into the test class

[GravityDI]
public class GravityCalculatorTests(IGravityCalculator calculator)
{
    [Test]
    [Arguments(100.0, Planet.Earth, 980.7)]
    [Arguments(100.0, Planet.Moon, 162.0)]
    [Arguments(100.0, Planet.Mars, 371.0)]
    public async Task CalculateForce_ReturnsCorrectForce(
        double mass, Planet planet, double expected)
    {
        var result = calculator.CalculateForce(mass, planet);
        await Assert.That(result).IsEqualTo(expected);
    }
}
Enter fullscreen mode Exit fullscreen mode

Result: tests consume dependencies exactly like production code.


Comparison with other popular test frameworks

This is exactly why the TUnit approach in your repo feels elegant: fewer moving parts, clear composition root, and constructor-injected tests.


Why this pattern scales

When app and tests share a single registration method:

  1. Refactors are safer (service graph changes once)
  2. Less setup duplication (lower maintenance)
  3. Cleaner tests (behavior-focused, not wiring-focused)

That is especially valuable in medium/large .NET solutions where DI registrations evolve frequently.


Try it quickly

git clone https://github.com/ConnectingApps/TUnitPublicDemo.git
cd TUnitPublicDemo
dotnet build
Enter fullscreen mode Exit fullscreen mode

Run tests:

cd Gravity.Test
dotnet run
Enter fullscreen mode Exit fullscreen mode

Run API:

cd Gravity
dotnet run
Enter fullscreen mode Exit fullscreen mode

Closing thought

If constructor injection is your production default, this TUnit model gives you the same ergonomics in tests—with minimal ceremony and better symmetry across the codebase.

If you want a concrete reference implementation, start here:
https://github.com/ConnectingApps/TUnitPublicDemo

Top comments (0)