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
- Basic Setup
- Traditional Fixture-Based Approach
- Property Injection (Recommended)
- Keyed Services
- Factory Pattern (Experimental)
- Configuration and User Secrets
- Advanced Dependency Injection Patterns
- Service Lifetimes
- Test Ordering
- Asynchronous Fixture Initialization
- 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
_fixture.GetService<T>(_testOutputHelper)- Gets a service instance_fixture.GetScopedService<T>(_testOutputHelper)- Gets a scoped service instance_fixture.GetKeyedService<T>("key", _testOutputHelper)- Gets a keyed service instance_fixture.GetAsyncScope(_testOutputHelper)- Gets an async service scope
Property Injection (Recommended)
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
- ✅ Clean, declarative syntax - Use
[Inject]attribute on properties - ✅ No manual fixture calls - Dependencies available immediately in test methods
- ✅ Full keyed services support - Both regular and keyed services work seamlessly
- ✅ Backward compatible - All existing
TestBed<TFixture>code continues to work unchanged - ✅ Gradual migration - Adopt new approach incrementally without breaking existing tests
Available Methods in Property Injection Approach
GetService<T>()- Gets a service instance (no_testOutputHelperparameter needed)GetScopedService<T>()- Gets a scoped service instanceGetKeyedService<T>("key")- Gets a keyed service instance
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
- Right-click your test project and select “Manage User Secrets”
- 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
- Use Property Injection for new projects - It provides the cleanest syntax
- Gradual Migration - You can mix both approaches in the same test suite
- Keyed Services - Use for multiple implementations of the same interface
- Configuration - Store non-sensitive data in
appsettings.json, sensitive data in user secrets - Service Lifetimes - Choose appropriate lifetimes based on your testing needs
- Factory Pattern - Use for true constructor injection when needed
- 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.