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
newcalls - 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;
}
}
2) Use it in production (Program.cs)
builder.Services.AddGravity();
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);
}
}
Result: tests consume dependencies exactly like production code.
Comparison with other popular test frameworks
- xUnit: DI is commonly achieved via external libraries and fixture-based patterns; xUnit itself does not natively provide a general-purpose test-class DI container model in the same way this TUnit pattern does.
-
NUnit: DI is generally extension-driven (e.g.,
nunit.dependencyinjection/ related packages), not a built-in, single canonical DI path for test class construction. - MSTest: DI patterns are commonly custom or sample-based rather than framework-native conventions.
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:
- Refactors are safer (service graph changes once)
- Less setup duplication (lower maintenance)
- 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
Run tests:
cd Gravity.Test
dotnet run
Run API:
cd Gravity
dotnet run
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)