Skip to the content.

Constructor Dependency Injection

This document describes the new constructor dependency injection capabilities added to the xUnit Dependency Injection framework while maintaining full backward compatibility with the existing fixture-based approach.

Overview

The framework now supports two approaches for dependency injection:

  1. Traditional Fixture-Based Approach (existing) - Access services via _fixture.GetService<T>(_testOutputHelper)
  2. Constructor Dependency Injection (new) - Inject services directly into test class properties during construction

Property Injection with TestBedWithDI

Basic Usage

Inherit from TestBedWithDI<TFixture> instead of TestBed<TFixture> and use the [Inject] attribute on properties:

public class PropertyInjectionTests : TestBedWithDI<TestProjectFixture>
{
    [Inject]
    public ICalculator? Calculator { get; set; }

    [Inject]
    public IOptions<Options>? Options { get; set; }

    public PropertyInjectionTests(ITestOutputHelper testOutputHelper, TestProjectFixture fixture)
        : base(testOutputHelper, fixture)
    {
        // Dependencies are automatically injected after base constructor completes
    }

    [Fact]
    public async Task TestCalculatorThroughPropertyInjection()
    {
        // Dependencies are already available - no need to call _fixture methods
        Assert.NotNull(Calculator);
        Assert.NotNull(Options);

        var result = await Calculator.AddAsync(5, 3);
        var expected = Options.Value.Rate * (5 + 3);
        Assert.Equal(expected, result);
    }
}

Keyed Services

Use the [Inject("key")] attribute for keyed services:

public class PropertyInjectionTests : TestBedWithDI<TestProjectFixture>
{
    [Inject("Porsche")]
    internal ICarMaker? PorscheCarMaker { get; set; }

    [Inject("Toyota")]
    internal ICarMaker? ToyotaCarMaker { get; set; }

    [Fact]
    public void TestKeyedServicesThroughPropertyInjection()
    {
        Assert.NotNull(PorscheCarMaker);
        Assert.NotNull(ToyotaCarMaker);
        Assert.Equal("Porsche", PorscheCarMaker.Manufacturer);
        Assert.Equal("Toyota", ToyotaCarMaker.Manufacturer);
    }
}

Convenience Methods

The TestBedWithDI class provides convenience methods that don’t require the _testOutputHelper parameter:

protected T? GetService<T>()
protected T? GetScopedService<T>()
protected T? GetKeyedService<T>(string key)
[Theory]
[InlineData(10, 20)]
public async Task TestConvenienceMethodsStillWork(int x, int y)
{
    // These methods are available without needing _fixture
    var calculator = GetService<ICalculator>();
    var options = GetService<IOptions<Options>>();
    var porsche = GetKeyedService<ICarMaker>("Porsche");

    Assert.NotNull(calculator);
    Assert.NotNull(options);
    Assert.NotNull(porsche);
}

Factory-Based Constructor Injection (Experimental)

For true constructor injection, use TestBedFactoryFixture with the factory pattern:

Setup

public class FactoryTestProjectFixture : TestBedFactoryFixture
{
    protected override void AddServices(IServiceCollection services, IConfiguration? configuration)
        => services
        .AddTransient<ICalculator, Calculator>()
        .AddKeyedTransient<ICarMaker, Porsche>("Porsche")
        .AddKeyedTransient<ICarMaker, Toyota>("Toyota")
        .AddTransient<SimpleService>(); // Register classes that need constructor injection
}

Usage

public class FactoryConstructorInjectionTests : TestBed<FactoryTestProjectFixture>
{
    [Fact]
    public async Task TestConstructorInjectionViaFactory()
    {
        // Create instances with constructor injection
        var simpleService = _fixture.CreateTestInstance<SimpleService>(_testOutputHelper);
        
        var result = await simpleService.CalculateAsync(10, 5);
        Assert.True(result > 0);
    }
}

Service Class with Constructor Injection

public class SimpleService
{
    private readonly ICalculator _calculator;
    private readonly Options _options;

    public SimpleService(ICalculator calculator, IOptions<Options> options)
    {
        _calculator = calculator ?? throw new ArgumentNullException(nameof(calculator));
        _options = options?.Value ?? throw new ArgumentNullException(nameof(options));
    }

    public async Task<int> CalculateAsync(int x, int y)
    {
        return await _calculator.AddAsync(x, y);
    }
}

Keyed Services in Factory Pattern

Use the [FromKeyedService("key")] attribute for keyed service constructor parameters:

public class CalculatorService
{
    public CalculatorService(
        ICalculator calculator,
        IOptions<Options> options,
        [FromKeyedService("Porsche")] ICarMaker porsche,
        [FromKeyedService("Toyota")] ICarMaker toyota)
    {
        // Constructor injection with keyed services
    }
}

Backward Compatibility

All existing code continues to work unchanged. The new approaches are additive:

Migration Path

You can migrate existing tests gradually:

  1. Option 1: Keep using TestBed<TFixture> with existing fixture methods
  2. Option 2: Change to TestBedWithDI<TFixture> and use [Inject] properties for new dependencies while keeping existing fixture method calls
  3. Option 3: Fully migrate to property injection for cleaner test code

Benefits

Property Injection Approach

Factory Approach

Recommendation

Use the Property Injection with TestBedWithDI approach for most scenarios as it provides the cleanest developer experience while maintaining full compatibility with the existing framework.