Use fixtures in xUnit for shared context in unit tests

Published: Monday 13 October 2025

When writing unit tests, it's common to share the same context across multiple tests. Instead of duplicating code in every test method, xUnit provides fixtures to share initialisation and cleanup logic.

In this article, we'll explore what fixtures are, why they're useful, and how to use them effectively in your test projects.

What we are testing

Throughout this article, we will be unit testing this Product class and the methods associated with it in the ProductHelper class:

// Product.cs
public record Product(int Id, string Name);
// ProductHelper.cs
public static class ProductHelper
{
	public static IList<Product> GetById(this IList<Product> products, int id)
	{
		return products.Where(p => p.Id == id).ToList();
	}

	public static IList<Product> GetByName(this IList<Product> products, string name)
	{
		return products.Where(p => p.Name == name).ToList();
	}
}

In our unit tests, we will be using the CSVHelper NuGet package to read files from this CSV file and add it to a List<Product> property that can be used in our unit tests.

"Id","Name"
"1","Watch"
"2","Necklace"
"3","Ring"
"4","Bracelet"
"5","Earrings"
"6","Charm"
"7","Pendant"
"8","Cuff links"
"9","Brooch"
"10","Bangle"

With the setup done, that's begin.

Sharing code for all tests in a test class

If you want to share code across multiple tests in the same test class, you can put the logic into the constructor. You can also implement the IDisposable interface for disposal after each test has completed.

Here, we are loading the Products.csv file into the test and using the CSVReader class from the CSVHelper package to store the list of products in a property.

// ProductTests.cs
public class ProductTests : IDisposable
{
	private string ProductCsvPath = $"{Environment.CurrentDirectory}/File/Products.csv";

	public IList<Product> Products { get; private set; }

	public ProductTests()
	{
		if (!File.Exists(ProductCsvPath))
		{
			throw new FileNotFoundException("Unable to find Product CSV file");
		}

		var fileContents = string.Empty;
		using var streamReader = new StreamReader(ProductCsvPath, new FileStreamOptions
		{
			Access = FileAccess.Read,
			Mode = FileMode.Open
		});

		using var csvReader = new CsvReader(streamReader, CultureInfo.InvariantCulture);

		Products = csvReader.GetRecords<Product>().ToList();
	}

	[Fact]
	public void GetByName_WithEarrings_ReturnsCorrectRecord()
	{
		var watchProduct = Assert.Single(Products.GetByName("Earrings"));

		Assert.Equal(5, watchProduct.Id);
		Assert.Equal("Earrings", watchProduct.Name);
	}

	[Fact]
	public void GetById_With5_ReturnsCorrectRecord()
	{
		var watchProduct = Assert.Single(Products.GetById(5));

		Assert.Equal(5, watchProduct.Id);
		Assert.Equal("Earrings", watchProduct.Name);
	}

	public void Dispose()
	{
		Products = new List<Product>();
	}
}

The issue with doing it this way is that the code in the constructor and Dispose method runs for every unit test in this class. This presents an unnecessary overhead if you could use the same context for each test.

Introducing fixtures

xUnit provides fixtures which allows you to share the same context across multiple tests. There are two main patterns available:

  • Class fixtures - Shared setup for all tests in a single test class
  • Collection fixtures - Shared setup across multiple test classes

Set up a fixture class

First we need to set up a fixture class. This will contain logic for the shared context across our tests. We have called this ProductFixture and it will read the products from the CSV file we mentioned above and store it in a property:

// ProductFixture.cs
public class ProductFixture : IDisposable
{
	private string ProductCsvPath = $"{Environment.CurrentDirectory}/File/Products.csv";

	public IList<Product> Products { get; private set; }

	public string Timestamp { get; }

	public ProductFixture()
	{
		Timestamp = DateTime.UtcNow.ToString("HH:mm:ss.ffffff");

		if (!File.Exists(ProductCsvPath))
		{
			throw new FileNotFoundException("Unable to find Product CSV file");
		}

		var fileContents = string.Empty;
		using var streamReader = new StreamReader(ProductCsvPath, new FileStreamOptions
		{
			Access = FileAccess.Read,
			Mode = FileMode.Open
		});

		using var csvReader = new CsvReader(streamReader, CultureInfo.InvariantCulture);

		Products = csvReader.GetRecords<Product>().ToList();
	}

	public void Dispose()
	{
		Products = new List<Product>();
	}
}

Class fixture

In order to use the ProductFixture class in a unit test class, we need to implement the IClassFixture interface with the ProductFixture type as its generic type. With this in place, we can inject the ProductFixture instance into the constructor of the test class and store it as a field.

// ProductClassFixtureTests.cs
public class ProductClassFixtureTests : IClassFixture<ProductFixture>
{
	private readonly ProductFixture _productFixture;

	public ProductClassFixtureTests(ProductFixture fixture)
	{
		_productFixture = fixture;
	}

	[Fact]
	public void ProductFixture_WhenCalled_VerifyProductsHaveLoaded()
	{
		Assert.True(_productFixture.Products.Count() == 10);
	}

	[Fact]
	public void GetByName_WithWatch_ReturnsOneRecord()
	{
		var watchProduct = Assert.Single(_productFixture.Products.GetByName("Watch"));

		Assert.Equal(1, watchProduct.Id);
		Assert.Equal("Watch", watchProduct.Name);
	}

	[Fact]
	public void GetByName_WithAbc_ReturnsNoRecords()
	{
		Assert.Empty(_productFixture.Products.GetByName("Abc"));
	}
}

It means that the code in the ProductFixture constructor will only run once regardless of how many tests are in the ProductClassFixtureTests class. It also means that the same ProductFixture instance can be used in each unit test within that class.

This is better because we are only reading the contents of the Products.csv file once, despite it being used in three unit tests.

Collection fixture

Sometimes, you need to share the same context across multiple test classes. Instead of repeating the fixture, you can use a collection fixture.

First of all, you need to set up an individual class that implements the ICollectionFixture interface and specifies the fixture class as the generic type. This is an empty class which has no unit tests and is decorated with the CollectionDefinition attribute.

// ProductCollection.cs
[CollectionDefinition("Product collection")]
public class ProductCollection : ICollectionFixture<ProductFixture>
{
}

You then decorate your unit test class with the Collection attribute using the same name that was given in the CollectionDefinition attribute of the empty class. This allows you to inject the fixture instance in your unit test class. But you don't implement the ICollectionFixture interface as this was implemented in the empty class:

[Collection("Product collection")]
public class ProductCollectionFixtureTests
{
	private readonly ProductFixture _productFixture;

	public ProductCollectionFixtureTests(ProductFixture productFixture)
	{
		_productFixture = productFixture;
	}

	[Fact]
	public void GetById_IdIs3_VerifyProperties()
	{
		var id3 = Assert.Single(_productFixture.Products.GetById(3));

		Assert.Equal(3, id3.Id);
		Assert.Equal("Ring", id3.Name);
	}
}

You can then set up multiple test classes by decorating it with the Collection attribute using the same name and injecting the fixture into your unit test.

// ProductCollection2FixtureTests.cs
[Collection("Product collection")]
public class ProductCollection2FixtureTests
{
	private readonly ProductFixture _productFixture;

	public ProductCollection2FixtureTests(ProductFixture productFixture)
	{
		_productFixture = productFixture;
	}

	[Fact]
	public void GetByName_NameIsRing_VerifyProperties()
	{
		var ringProduct = Assert.Single(_productFixture.Products.GetByName("Ring"));

		Assert.Equal(3, ringProduct.Id);
		Assert.Equal("Ring", ringProduct.Name);
	}
}

For the examples above, it means that all the unit tests in the ProductCollectionFixtureTests and ProductCollection2FixtureTests test classes will only run the code that is in the constructor of the ProductFixture class once and will be disposed when all the tests have been completed.

Watch the video

When you watch this video, you'll learn about the differences between using class and collection fixtures and get to see how they work:

And when you download the code example, you'll be able to try out class and collection fixtures for yourself.

Beware of modifying the shared state

Beware of modifying anything in the shared state as it could break other tests. For example, if you were to add a new product in the List<Product> type for one of the unit tests, it could break another unit test that counts the number of elements contained within it.

Also make sure that you take advantage of IDisposable for anything that needs cleaning up after you've finished with the shared context, such as closing a connection.