Setup, Fixtures and Lifetime
The constructor and Dispose run per test; IClassFixture shares expensive setup, and test classes run in parallel by default.
public class MyTests : IClassFixture<MyFixture>, IDisposablexUnit creates a brand new instance of your test class for every test method in it. So the constructor is the setup step, and if the class implements IDisposable, its Dispose method is the cleanup step. There is no separate setup attribute, because the constructor already is one. A fresh instance per test means one test cannot leave a half-modified field behind that changes the next test's result.
Sharing something expensive
Some setup is too slow to repeat for every test: starting a database, loading a large file, building a web application. Put that work in its own class, called a fixture, and have the test class implement IClassFixture<TFixture>. xUnit builds the fixture once, hands the same instance to every test class instance through the constructor, and disposes it after the last test in that class. ICollectionFixture<TFixture> does the same across several classes that join a named collection with [Collection("name")].
What xUnit guarantees about parallelism
Each test class is its own collection by default. xUnit runs different collections in parallel, and runs the tests inside one collection one at a time. So two tests in the same class never overlap, but two tests in different classes can and will.
That is where shared state bites. A fixed file path, a fixed TCP port or a shared database row used by two test classes will fail intermittently. Either give each class its own, or put both classes in one collection so they are serialised.
Example
using System;
using System.IO;
using Xunit;
namespace Inventory.Tests;
// Built once for the whole test class, then disposed.
public class SeedFilesFixture : IDisposable
{
public SeedFilesFixture()
{
Root = Path.Combine(Path.GetTempPath(), "inventory-seed");
Directory.CreateDirectory(Root);
File.WriteAllText(Path.Combine(Root, "items.txt"), "bolt\nnut\n");
}
public string Root { get; }
public void Dispose()
{
Directory.Delete(Root, recursive: true);
}
}
public class ItemFileTests : IClassFixture<SeedFilesFixture>, IDisposable
{
private readonly string _itemsPath;
private readonly string _scratchPath;
// Runs before EVERY test in this class; the fixture is created only once.
public ItemFileTests(SeedFilesFixture seed)
{
_itemsPath = Path.Combine(seed.Root, "items.txt");
_scratchPath = Path.Combine(seed.Root, "scratch.txt");
File.WriteAllText(_scratchPath, string.Empty);
}
// Runs after EVERY test in this class.
public void Dispose()
{
File.Delete(_scratchPath);
}
[Fact]
public void SeedFile_HasTwoItems()
{
string[] lines = File.ReadAllLines(_itemsPath);
Assert.Equal(2, lines.Length);
}
[Fact]
public void ScratchFile_StartsEmpty()
{
Assert.Empty(File.ReadAllText(_scratchPath));
}
}
// dotnet test reports: Failed: 0, Passed: 2, Skipped: 0, Total: 2When to use it
- Create and delete a temporary working folder around every test in a class.
- Start one in-memory database or container for a whole class of tests instead of one per test.
- Serialise two test classes that both write to the same development database.
More examples
Sharing one fixture across several classes
Use this when the expensive thing is expensive enough that one per class is still too many, such as a real database container. Two side effects come with it: the fixture now lives until the last class in the collection finishes, and the classes in the collection no longer run in parallel against each other. That second effect is often the real reason people reach for a collection.
using Xunit;
namespace Inventory.Tests;
// The definition class carries the interface and holds no tests itself.
[CollectionDefinition("seed files")]
public class SeedFilesCollection : ICollectionFixture<SeedFilesFixture>
{
}
[Collection("seed files")]
public class ReadTests
{
private readonly SeedFilesFixture _seed;
public ReadTests(SeedFilesFixture seed)
{
_seed = seed;
}
[Fact]
public void Root_Exists()
{
Assert.False(string.IsNullOrEmpty(_seed.Root));
}
}
[Collection("seed files")]
public class WriteTests
{
private readonly SeedFilesFixture _seed;
public WriteTests(SeedFilesFixture seed)
{
_seed = seed;
}
[Fact]
public void Root_IsTheSameFolder()
{
Assert.False(string.IsNullOrEmpty(_seed.Root));
}
}Turning parallelism off when you have to
The escape hatch for a suite that shares a resource you cannot duplicate, such as one development database or an environment variable. The same settings exist as parallelizeTestCollections and maxParallelThreads in an xunit.runner.json file, if you prefer configuration over an attribute. Treat it as a last resort: it hides the shared state rather than removing it, and the whole suite gets slower.
// AssemblyInfo.cs in the test project.
using Xunit;
// Nothing in this assembly runs in parallel with anything else.
[assembly: CollectionBehavior(DisableTestParallelization = true)]
// Or keep parallelism and just cap the worker threads:
// [assembly: CollectionBehavior(MaxParallelThreads = 2)]Per-test state versus shared state
This is the behaviour people mistrust until they see it: the first test adds an item and the second still sees an empty list. A field initialiser runs as part of the constructor, so it is per-test setup too. Note new() with no type after it is shorthand for new List<string>(); the type comes from the field declaration and the two forms compile to the same thing. Make a field static and the isolation disappears, which is the usual cause of tests that pass alone and fail together.
using System.Collections.Generic;
using Xunit;
namespace Inventory.Tests;
public class BasketTests
{
// New list for each test, because the class is constructed per test.
private readonly List<string> _basket = new();
[Fact]
public void Add_OneItem_LeavesOneItem()
{
_basket.Add("bolt");
Assert.Single(_basket);
}
[Fact]
public void NewBasket_IsEmpty()
{
// Passes because the previous test got its own instance.
Assert.Empty(_basket);
}
}
Discussion