Skip to the content.

Xunit Dependency Injection - Comprehensive Examples

This document provides comprehensive examples demonstrating all the ways to use the Xunit.Microsoft.DependencyInjection library. All examples are taken from working test code in the examples/ directory.

Table of Contents

  1. Basic Setup
  2. Traditional Fixture-Based Approach
  3. Property Injection (Recommended)
  4. Keyed Services
  5. Factory Pattern (Experimental)
  6. Configuration and User Secrets
  7. Advanced Dependency Injection Patterns
  8. Service Lifetimes
  9. Test Ordering
  10. Asynchronous Fixture Initialization
  11. xUnit.net v4 Features

Basic Setup

1. Creating a Test Fixture

First, create a test fixture that derives from TestBedFixture:

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Xunit.Microsoft.DependencyInjection.Abstracts;

public class TestProjectFixture : TestBedFixture
{
    protected override void AddServices(IServiceCollection services, IConfiguration? configuration)
        => services
        // Transient services - new instance for each injection
        .AddTransient<ICalculator, Calculator>()
        .AddTransient<ITransientService, TransientService>()
        .AddKeyedTransient<ICarMaker, Porsche>("Porsche")
        .AddKeyedTransient<ICarMaker, Toyota>("Toyota")

        // Scoped services - same instance within a scope (test)
        .AddScoped<IScopedService, ScopedService>()

        // Singleton services - same instance across entire application lifetime
        .AddSingleton<ISingletonService, SingletonService>()

        // Configure options
        .Configure<Options>(config => configuration?.GetSection("Options").Bind(config))
        .Configure<SecretValues>(config => configuration?.GetSection(nameof(SecretValues)).Bind(config));

    protected override IEnumerable<TestAppSettings> GetTestAppSettings()
    {
        yield return new() { Filename = "appsettings.json", IsOptional = false };
    }

    protected override void AddUserSecrets(IConfigurationBuilder configurationBuilder)
        => configurationBuilder.AddUserSecrets<TestProjectFixture>();
}

2. Configuration File

Create an appsettings.json file in your test project:

{
  "Options": {
    "Rate": 10
  },
  "SecretValues": {
    "Secret1": "StoreSecret1InUserSecrets",
    "Secret2": "StoreSecret2InUserSecrets"
  }
}

3. Example Services

public interface ICalculator
{
    Task<int> AddAsync(int x, int y);
}

public class Calculator : ICalculator
{
    private readonly Options _option;
    private readonly ILogger<Calculator> _logger;

    public Calculator(ILogger<Calculator> logger, IOptions<Options> option)
    {
        _option = option.Value;
        _logger = logger;
    }

    public Task<int> AddAsync(int x, int y)
    {
        var result = (x + y) * _option.Rate;
        _logger.LogInformation("The result is {@Result}", result);
        return Task.FromResult(result);
    }
}

public class Options
{
    public int Rate { get; set; }
}

Traditional Fixture-Based Approach

This is the classic approach that works with all versions of the library:

using Microsoft.Extensions.Options;
using Xunit.Microsoft.DependencyInjection.Abstracts;

public class CalculatorTests : TestBed<TestProjectFixture>
{
    private readonly Options _options;

    public CalculatorTests(ITestOutputHelper testOutputHelper, TestProjectFixture fixture)
        : base(testOutputHelper, fixture) 
    {
        _options = _fixture.GetService<IOptions<Options>>(_testOutputHelper)!.Value;
    }

    [Theory]
    [InlineData(1, 2)]
    public async Task TestServiceAsync(int x, int y)
    {
        // Get service from fixture
        var calculator = _fixture.GetService<ICalculator>(_testOutputHelper)!;
        
        // Use the service
        var calculatedValue = await calculator.AddAsync(x, y);
        var expected = _options.Rate * (x + y);
        
        Assert.Equal(expected, calculatedValue);
    }

    [Theory]
    [InlineData(1, 2)]
    public async Task TestScopedServiceAsync(int x, int y)
    {
        // Get scoped service from fixture
        var calculator = _fixture.GetScopedService<ICalculator>(_testOutputHelper)!;
        
        var calculatedValue = await calculator.AddAsync(x, y);
        var expected = _options.Rate * (x + y);
        
        Assert.Equal(expected, calculatedValue);
    }
}

Available Methods in Traditional Approach

New in version 9.2.0+: Clean, declarative syntax using property injection with the [Inject] attribute:

using Microsoft.Extensions.Options;
using Xunit.Microsoft.DependencyInjection.Abstracts;
using Xunit.Microsoft.DependencyInjection.Attributes;

/// <summary>
/// Example tests demonstrating property injection using the new TestBedWithDI base class
/// </summary>
public class PropertyInjectionTests : TestBedWithDI<TestProjectFixture>
{
    // Regular service injection
    [Inject]
    public ICalculator? Calculator { get; set; }

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

    // Keyed service injection
    [Inject("Porsche")]
    internal ICarMaker? PorscheCarMaker { get; set; }

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

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

    [Fact]
    public async Task TestCalculatorThroughPropertyInjection()
    {
        // Arrange - dependencies are already injected via properties
        Assert.NotNull(Calculator);
        Assert.NotNull(Options);

        // Act
        var result = await Calculator.AddAsync(5, 3);

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

    [Fact]
    public void TestKeyedServicesThroughPropertyInjection()
    {
        // Arrange - keyed services are already injected via properties
        Assert.NotNull(PorscheCarMaker);
        Assert.NotNull(ToyotaCarMaker);

        // Assert
        Assert.Equal("Porsche", PorscheCarMaker.Manufacturer);
        Assert.Equal("Toyota", ToyotaCarMaker.Manufacturer);
    }

    [Theory]
    [InlineData(10, 20)]
    public async Task TestConvenienceMethodsStillWork(int x, int y)
    {
        // Demonstrate that convenience methods from the base class still work
        var calculator = GetService<ICalculator>();
        var options = GetService<IOptions<Options>>();
        var porsche = GetKeyedService<ICarMaker>("Porsche");

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

        var result = await calculator.AddAsync(x, y);
        var expected = options.Value.Rate * (x + y);
        Assert.Equal(expected, result);
    }
}

Benefits of Property Injection

Available Methods in Property Injection Approach

Keyed Services

Keyed services let you register multiple implementations of the same interface under different keys. They were introduced in .NET 8 and are fully supported by this library on .NET 10.0:

Traditional Approach with Keyed Services

public class KeyedServicesTests : TestBed<TestProjectFixture>
{
    public KeyedServicesTests(ITestOutputHelper testOutputHelper, TestProjectFixture fixture) 
        : base(testOutputHelper, fixture)
    {
    }

    [Theory]
    [InlineData("Porsche")]
    [InlineData("Toyota")]
    public void GetKeyedService(string key)
    {
        var carMaker = _fixture.GetKeyedService<ICarMaker>(key, _testOutputHelper)!;
        Assert.Equal(key, carMaker.Manufacturer);
    }
}

Property Injection with Keyed Services

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

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

    // ... constructor and other code ...

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

Keyed Service Registration

protected override void AddServices(IServiceCollection services, IConfiguration? configuration)
    => services
    .AddKeyedTransient<ICarMaker, Porsche>("Porsche")
    .AddKeyedTransient<ICarMaker, Toyota>("Toyota");

public interface ICarMaker
{
    string Manufacturer { get; }
}

public class Porsche : ICarMaker
{
    public string Manufacturer => "Porsche";
}

public class Toyota : ICarMaker
{
    public string Manufacturer => "Toyota";
}

Factory Pattern (Experimental)

For true constructor injection into service classes, you can use the factory pattern:

Factory Fixture Setup

public class FactoryTestProjectFixture : TestBedFixture
{
    protected override void AddServices(IServiceCollection services, IConfiguration? configuration)
        => services
        .AddTransient<ICalculator, Calculator>()
        .AddKeyedTransient<ICarMaker, Porsche>("Porsche")
        .AddKeyedTransient<ICarMaker, Toyota>("Toyota")
        .Configure<Options>(config => configuration?.GetSection("Options").Bind(config));

    // Same implementation as TestProjectFixture for other methods...
}

Factory Tests

/// <summary>
/// Example tests demonstrating factory-based constructor injection
/// This approach allows for true constructor injection by creating instances via the fixture factory
/// </summary>
public class FactoryConstructorInjectionTests : TestBed<FactoryTestProjectFixture>
{
    public FactoryConstructorInjectionTests(ITestOutputHelper testOutputHelper, FactoryTestProjectFixture fixture)
        : base(testOutputHelper, fixture)
    {
    }

    [Fact]
    public async Task TestSimpleConstructorInjectionViaFactory()
    {
        // Arrange - Create instance with constructor injection via factory
        var simpleService = _fixture.CreateTestInstance<SimpleService>(_testOutputHelper);

        // Act
        var result = await simpleService.CalculateAsync(10, 5);
        var rate = simpleService.GetRate();

        // Assert
        var expected = rate * (10 + 5);
        Assert.Equal(expected, result);
    }

    [Fact]
    public void TestFactoryWithAdditionalParameters()
    {
        // Create a custom test class that needs both DI services and custom parameters
        var testString = "test-data";
        var testInstance = _fixture.CreateTestInstance<CustomTestClass>(_testOutputHelper, testString);

        Assert.NotNull(testInstance.Calculator);
        Assert.Equal(testString, testInstance.CustomData);
    }
}

/// <summary>
/// Example class that demonstrates constructor injection with both DI services
/// and custom parameters
/// </summary>
public class CustomTestClass
{
    public ICalculator Calculator { get; }
    public string CustomData { get; }

    public CustomTestClass(ICalculator calculator, string customData)
    {
        Calculator = calculator ?? throw new ArgumentNullException(nameof(calculator));
        CustomData = customData ?? throw new ArgumentNullException(nameof(customData));
    }
}

Configuration and User Secrets

Configuration Setup

The library supports configuration files and user secrets for sensitive data:

protected override IEnumerable<TestAppSettings> GetTestAppSettings()
{
    yield return new() { Filename = "appsettings.json", IsOptional = false };
}

protected override void AddUserSecrets(IConfigurationBuilder configurationBuilder)
    => configurationBuilder.AddUserSecrets<TestProjectFixture>();

Using Configuration in Tests

public class UserSecretTests : TestBed<TestProjectFixture>
{
    public UserSecretTests(ITestOutputHelper testOutputHelper, TestProjectFixture fixture) 
        : base(testOutputHelper, fixture)
    {
    }

    [Fact]
    public void TestSecretValues()
    {
        /*
         * Create a user secret entry like the following payload in user secrets:
         * 
         * "SecretValues": {
         *   "Secret1": "secret1value",
         *   "Secret2": "secret2value"
         * }
         */
        var secretValues = _fixture.GetService<IOptions<SecretValues>>(_testOutputHelper)!.Value;
        Assert.NotEmpty(secretValues?.Secret1 ?? string.Empty);
        Assert.NotEmpty(secretValues?.Secret2 ?? string.Empty);
    }
}

public record SecretValues
{
    public string? Secret1 { get; set; }
    public string? Secret2 { get; set; }
}

Setting Up User Secrets

  1. Right-click your test project and select “Manage User Secrets”
  2. Add your secret configuration:
{
  "SecretValues": {
    "Secret1": "secret1value",
    "Secret2": "secret2value"
  }
}

Advanced Dependency Injection Patterns

IOptions Pattern

public class AdvancedDependencyInjectionTests : TestBedWithDI<TestProjectFixture>
{
    [Inject]
    public IOptions<Options>? Options { get; set; }

    [Fact]
    public void TestOptionsPattern()
    {
        Assert.NotNull(Options);
        Assert.True(Options.Value.Rate > 0);
    }
}

Func Factory Pattern

Register and use service factories:

// In fixture
protected override void AddServices(IServiceCollection services, IConfiguration? configuration)
    => services
    .AddTransient<ICalculator, Calculator>()
    .AddTransient<Func<ICalculator>>(provider => () => provider.GetService<ICalculator>()!);

// In tests
[Inject]
public Func<ICalculator>? CalculatorFactory { get; set; }

[Fact]
public async Task TestFactoryPattern()
{
    Assert.NotNull(CalculatorFactory);
    
    var calculator = CalculatorFactory();
    var result = await calculator.AddAsync(1, 2);
    
    Assert.True(result > 0);
}

Action Pattern

[Fact]
public void TestActionTPatternWithServices()
{
    var calculatorResults = new List<int>();
    
    Action<ICalculator> calculatorAction = async calc =>
    {
        var result = await calc.AddAsync(10, 5);
        calculatorResults.Add(result);
    };

    // Use the action with injected calculator
    calculatorAction(Calculator!);
    
    Assert.Single(calculatorResults);
    Assert.True(calculatorResults[0] > 0);
}

Service Lifetimes

Transient Services

New instance for each injection:

public class TransientServiceTests : TestBed<TestProjectFixture>
{
    [Fact]
    public void TestTransientServicesAreDifferentInstances()
    {
        var service1 = _fixture.GetService<ITransientService>(_testOutputHelper)!;
        var service2 = _fixture.GetService<ITransientService>(_testOutputHelper)!;

        Assert.NotEqual(service1.InstanceId, service2.InstanceId);
    }
}

Scoped Services

Same instance within a scope (test):

public class ScopedServiceTests : TestBed<TestProjectFixture>
{
    [Fact]
    public void TestScopedServicesAreSameInstanceWithinScope()
    {
        var service1 = _fixture.GetScopedService<IScopedService>(_testOutputHelper)!;
        var service2 = _fixture.GetScopedService<IScopedService>(_testOutputHelper)!;

        Assert.Equal(service1.InstanceId, service2.InstanceId);
    }
}

Singleton Services

Same instance across entire application lifetime:

public class SingletonServiceTests : TestBed<TestProjectFixture>
{
    [Fact]
    public void TestSingletonServicesAreSameInstance()
    {
        var service1 = _fixture.GetService<ISingletonService>(_testOutputHelper)!;
        var service2 = _fixture.GetService<ISingletonService>(_testOutputHelper)!;

        Assert.Equal(service1.InstanceId, service2.InstanceId);
    }
}

Test Ordering

The library provides a bonus feature for running tests in order:

[TestCaseOrderer(typeof(TestPriorityOrderer))]   // or [TestCaseOrderer<TestPriorityOrderer>] on xUnit.net v4
public class UnitTests : TestBed<TestProjectFixture>
{
    public UnitTests(ITestOutputHelper testOutputHelper, TestProjectFixture fixture) 
        : base(testOutputHelper, fixture)
    {
    }

    [Fact, TestOrder(1)]
    public async Task Test1()
    {
        var calculator = _fixture.GetService<ICalculator>(_testOutputHelper)!;
        var result = await calculator.AddAsync(1, 2);
        Assert.True(result > 0);
    }

    [Fact, TestOrder(2)]
    public async Task Test2()
    {
        var calculator = _fixture.GetService<ICalculator>(_testOutputHelper)!;
        var result = await calculator.AddAsync(3, 4);
        Assert.True(result > 0);
    }

    [Theory, TestOrder(3)]
    [InlineData(5, 6)]
    public async Task Test3(int x, int y)
    {
        var calculator = _fixture.GetService<ICalculator>(_testOutputHelper)!;
        var result = await calculator.AddAsync(x, y);
        Assert.True(result > 0);
    }
}

Asynchronous Fixture Initialization

TestBedFixture implements xUnit.net’s IAsyncLifetime. Override InitializeAsyncCore() for setup that must be awaited before any test uses the fixture - starting a Testcontainer, seeding a database, fetching remote configuration. xUnit.net awaits it once after construction, and because the container is built lazily on first use, values produced here can feed the registrations in AddServices:

public class AsyncInitFixture : TestBedFixture
{
    private string? _connectionString;

    protected override async ValueTask InitializeAsyncCore()
    {
        // Stands in for genuinely asynchronous work: starting a Testcontainer,
        // seeding a database, fetching configuration from a remote source, etc.
        await Task.Yield();
        _connectionString = "Server=initialized-async";
    }

    protected override void AddServices(IServiceCollection services, IConfiguration configuration)
        => services.AddSingleton(new AsyncInitOptions(_connectionString!));
}
public class AsyncInitTests(ITestOutputHelper testOutputHelper, AsyncInitFixture fixture)
    : TestBed<AsyncInitFixture>(testOutputHelper, fixture)
{
    [Fact]
    public void ValueProducedDuringInitializationIsRegisteredInTheContainer()
    {
        var options = _fixture.GetService<AsyncInitOptions>(_testOutputHelper);

        Assert.NotNull(options);
        Assert.Equal("Server=initialized-async", options.ConnectionString);
    }
}

The container does not exist while InitializeAsyncCore runs, so it cannot resolve services - it prepares the inputs that AddServices registers. Pair it with DisposeAsyncCore() for teardown; both are virtual no-ops by default. The full example lives in Fixtures/AsyncInitFixture.cs and AsyncInitTests.cs.

xUnit.net v4 Features

These examples target xunit.v3 4.0.0 and Xunit.Microsoft.DependencyInjection 10.1.0 or later. Remember that v4 runs on Microsoft Testing Platform, so your repository needs a global.json containing:

{
  "test": {
    "runner": "Microsoft.Testing.Platform"
  }
}

Fixture lifecycle notifications

A fixture can react to the test class that consumes it, without introducing a second fixture type:

using Xunit.v3;

public class LifecycleAwareFixture : TestBedFixture, INotifyTestClassLifecycleAsync
{
    private readonly List<string> _startedClasses = [];

    public IReadOnlyList<string> StartedClasses => _startedClasses;

    public ValueTask OnTestClassStartingAsync(IXunitTestClass testClass)
    {
        _startedClasses.Add(testClass.TestClassSimpleName);
        return default;
    }

    public ValueTask OnTestClassFinishedAsync(IXunitTestClass testClass) => default;

    protected override void AddServices(IServiceCollection services, IConfiguration configuration)
        => services.AddSingleton<ICalculator, Calculator>();
}
public class LifecycleNotificationTests : TestBedWithDI<LifecycleAwareFixture>
{
    private readonly LifecycleAwareFixture _lifecycleFixture;

    public LifecycleNotificationTests(ITestOutputHelper testOutputHelper, LifecycleAwareFixture fixture)
        : base(testOutputHelper, fixture) => _lifecycleFixture = fixture;

    [Fact]
    public void FixtureIsNotifiedThatTheTestClassStarted()
        => Assert.Contains(nameof(LifecycleNotificationTests), _lifecycleFixture.StartedClasses);
}

Equivalents exist for the assembly, collection, method, test and test case levels, in both synchronous (INotifyTestClassLifecycle) and asynchronous forms.

Running every test in parallel

ParallelMode.All runs all tests concurrently, including tests that share a fixture. Enable it in testconfig.json at the root of your test project:

{
  "xUnit": {
    "parallelMode": "all"
  }
}

TestBedFixture builds its container under a lock, so concurrent first access still produces a single ServiceProvider:

[Fact]
public void GetServiceProvider_ConcurrentFirstAccess_BuildsSingleProvider()
{
    using var fixture = new TestProjectFixture();
    var outputHelper = TestContext.Current.TestOutputHelper!;

    var providers = new ServiceProvider[64];
    Parallel.For(0, providers.Length, i => providers[i] = fixture.GetServiceProvider(outputHelper));

    Assert.Single(providers.Distinct());
}

What the library cannot make safe for you is shared state. [Inject] and GetService<T>() resolve from the fixture’s root container, so a stateful service is shared by every test using that fixture and those tests now run at the same time. Either resolve per-test state through a scope:

var scoped = GetScopedService<IScopedService>();   // fresh instance per call

…or opt the class out of parallelization:

[TestClass(DisableParallelization = true)]
public class ScopedServiceTests : TestBedWithDI<TestProjectFixture> { }

DisableParallelization is also available on [CollectionDefinition], [Fact], [Theory] and theory data rows. Once disabled at one layer it cannot be re-enabled below it.

Ordering test classes and methods

Alongside the existing collection and case orderers, v4 adds ITestClassOrderer and ITestMethodOrderer, applied via [TestClassOrderer<TOrderer>] and [TestMethodOrderer<TOrderer>]. Ordering runs collection → class → method → case.

Best Practices

  1. Use Property Injection for new projects - It provides the cleanest syntax
  2. Gradual Migration - You can mix both approaches in the same test suite
  3. Keyed Services - Use for multiple implementations of the same interface
  4. Configuration - Store non-sensitive data in appsettings.json, sensitive data in user secrets
  5. Service Lifetimes - Choose appropriate lifetimes based on your testing needs
  6. Factory Pattern - Use for true constructor injection when needed
  7. Test Ordering - Use sparingly, only when tests have dependencies on each other

Migration Guide

From Traditional to Property Injection

Before:

public class MyTests : TestBed<TestProjectFixture>
{
    [Fact]
    public async Task TestCalculation()
    {
        var calculator = _fixture.GetService<ICalculator>(_testOutputHelper);
        var result = await calculator.AddAsync(1, 2);
        Assert.Equal(3, result);
    }
}

After:

public class MyTests : TestBedWithDI<TestProjectFixture>
{
    [Inject] private ICalculator Calculator { get; set; } = null!;
    
    [Fact]
    public async Task TestCalculation()
    {
        var result = await Calculator.AddAsync(1, 2);
        Assert.Equal(3, result);
    }
}

This concludes the comprehensive examples for the Xunit.Microsoft.DependencyInjection library. For more details, visit the repository examples.