In this lesson, you will learn how to optimize your test suite speed by identifying and offloading heavy initialization tasks from individual test methods.
When you write unit tests, it is common to perform setup work—such as seeding complex database objects, initializing dependency injection containers, or spinning up local services—inside the test method itself. While this makes tests readable, repeating these expensive operations for every single [Fact] leads to a linear increase in test execution time. As your project grows, this creates a "performance tax" that discourages frequent local test runs.
The Problem: Redundant Heavy Lifting
Consider a common pattern where each test method recreates the state of an entire service layer:
[Fact]
public void CalculateDiscount_ValidUser_ReturnsCorrectValue()
{
// Heavy: Re-seeding an entire database state or re-instantiating
// a complex dependency graph for every single test
var context = CreateFullDatabaseInstance();
var service = new OrderService(context);
// ... test logic
}
If you have 100 tests, CreateFullDatabaseInstance() runs 100 times. If that method takes 50ms, you have already burned 5 seconds just on infrastructure overhead.
The Solution: Shift to Class-Level Fixtures
To refactor for performance, move heavy setup tasks to the IClassFixture<T> or ICollectionFixture<T> patterns provided by xUnit. By moving the heavy lifting to the constructor of a shared fixture class, the setup runs once per test class rather than once per test method.
public class OrderServiceFixture
{
public DatabaseContext Context { get; }
public OrderServiceFixture() => Context = InitializeHeavyResources();
}
public class OrderServiceTests : IClassFixture<OrderServiceFixture>
{
private readonly OrderServiceFixture _fixture;
public OrderServiceTests(OrderServiceFixture fixture) => _fixture = fixture;
[Fact]
public void CalculateDiscount_ValidUser_ReturnsCorrectValue()
=> Assert.NotNull(_fixture.Context);
}
By extracting the heavy lifting into a fixture, you significantly reduce the runtime of your test suite without sacrificing the integrity of the individual test cases. Always look for setup code that doesn't strictly need to vary between tests and move it "up" the lifecycle to the highest level possible.
Key Takeaway: Move expensive setup tasks from the test method body into a class or collection fixture to execute heavy initialization once per suite rather than once per test.