xUnit.net
The .NET runner with a fresh instance per test — Facts, Theories, fixtures, parallelism and the design opinions baked into it.
1 min read · updated 19 September 2026
xUnit.net is the default .NET test framework and the one most new projects use. Its distinguishing feature is an opinion: a new instance of the test class per test.
#Facts and theories
public class PricingTests
{
[Fact]
public void An_empty_basket_costs_nothing()
{
Assert.Equal(0, new Basket().TotalCents);
}
// A theory is a parameterised test; each row is reported separately.
[Theory]
[InlineData(12_000, 2_400)]
[InlineData(10_000, 2_000)] // the boundary
[InlineData(9_999, 0)]
public void Discount_applies_at_or_above_the_threshold(int subtotal, int expected)
{
var result = Pricing.ApplyDiscount(new Order(subtotal), new DiscountPolicy(10_000, 20));
Assert.Equal(expected, result.DiscountCents);
}
// MemberData when the cases are not compile-time constants.
public static IEnumerable<object[]> ShippingCases =>
[
[CustomerTier.Standard, 395],
[CustomerTier.Gold, 0]
];
[Theory]
[MemberData(nameof(ShippingCases))]
public void Shipping_depends_on_tier(CustomerTier tier, int expectedCents)
{
Assert.Equal(expectedCents, Shipping.For(new Customer(tier)).Cents);
}
}#Setup and teardown
There is no [SetUp]. The constructor runs before each test, Dispose
after, and IAsyncLifetime covers the async case.
public class OrderRepositoryTests : IAsyncLifetime
{
private NpgsqlConnection _connection = null!;
private NpgsqlTransaction _transaction = null!;
public async Task InitializeAsync()
{
_connection = new NpgsqlConnection(TestDatabase.ConnectionString);
await _connection.OpenAsync();
_transaction = await _connection.BeginTransactionAsync();
}
// Rolled back, never committed: perfect isolation, no cleanup code.
public async Task DisposeAsync() => await _transaction.RollbackAsync();
}#Fixtures: sharing expensive setup
// One container for the whole class.
public sealed class PostgresFixture : IAsyncLifetime
{
private readonly PostgreSqlContainer _container =
new PostgreSqlBuilder().WithImage("postgres:16-alpine").Build();
public string ConnectionString => _container.GetConnectionString();
public async Task InitializeAsync()
{
await _container.StartAsync();
await Migrations.ApplyAsync(ConnectionString);
}
public Task DisposeAsync() => _container.DisposeAsync().AsTask();
}
public class OrderRepositoryTests(PostgresFixture fixture) : IClassFixture<PostgresFixture>
{
[Fact]
public async Task Round_trips_money_without_losing_precision() { /* ... */ }
}
// One container shared by many classes.
[CollectionDefinition("database")]
public class DatabaseCollection : ICollectionFixture<PostgresFixture>;
[Collection("database")]
public class CustomerRepositoryTests(PostgresFixture fixture) { }See Testcontainers for the container side of this.
#Parallelism
Test collections run in parallel; tests within a collection do not. Every class is its own collection by default, so classes run concurrently out of the box.
// Opt a set of classes into running sequentially by naming a collection.
[Collection("Sequential")]
public class MigrationTests { }<!-- xunit.runner.json / csproj -->
<PropertyGroup>
<ParallelizeTestCollections>true</ParallelizeTestCollections>
<MaxParallelThreads>4</MaxParallelThreads>
</PropertyGroup>Needing [Collection("Sequential")] is a smell — usually shared state that
should be scoped instead. See
parallel test execution.
#Assertions
xUnit's assertion set is deliberately small. Most teams add FluentAssertions or Shouldly for readability:
// xUnit built-in
Assert.Equal(2_400, result.DiscountCents);
Assert.Contains(order.Lines, l => l.Sku == "book-1");
await Assert.ThrowsAsync<InsufficientStockException>(() => service.ReserveAsync(order));
// FluentAssertions — better failure messages, worth the dependency
result.DiscountCents.Should().Be(2_400);
order.Lines.Should().ContainSingle(l => l.Sku == "book-1");
await service.Invoking(s => s.ReserveAsync(order))
.Should().ThrowAsync<InsufficientStockException>()
.WithMessage("*book-1*");Note FluentAssertions changed to a commercial licence for version 8; version 7 remains free. Shouldly is the usual alternative if that matters to you.
#Useful details
// Skip with a reason — it shows up in the report rather than vanishing.
[Fact(Skip = "ENG-4412: flaky under parallel load, owner @sam, review 2026-10-15")]
// Traits for filtering: dotnet test --filter "Category=Integration"
[Trait("Category", "Integration")]
// Output that appears in the test report, not the console.
public class MyTests(ITestOutputHelper output)
{
[Fact] public void Thing() => output.WriteLine("diagnostic");
}#Choosing between the three .NET runners
NUnit has a richer assertion and attribute set and shares one instance across the tests in a class, which makes accidental coupling easier. MSTest is the Microsoft-supplied one, fine and unremarkable. xUnit's fresh-instance model is the reason to prefer it: it removes a class of bug rather than documenting it.
For doubles, see Moq and mocking frameworks. For running a whole ASP.NET application in-process, see component testing.
Common questions
- Why does xUnit create a new instance of the test class for each test?
- To make shared mutable state between tests impossible by construction. It is an opinion rather than a technical necessity, and it is the main reason xUnit suites tend to have fewer order dependencies than NUnit ones.
- What replaced SetUp and TearDown in xUnit?
- The constructor and IDisposable (or IAsyncLifetime for async work). Since a new instance is created per test, the constructor is the per-test setup and Dispose is the teardown.
- How do I share an expensive fixture between tests in xUnit?
- IClassFixture<T> shares one instance across a class, ICollectionFixture<T> across a collection of classes. Both are created once and disposed at the end, which is how you start a database container for a whole suite.
Runnable samples for this page
last test results ↗- C#
dotnet/Tests.XUnit/tools/xunit
Working tests, not fragments — they run in CI on every push to 8exgh/endtoendtester-samples.
Was this page useful?
Related topics
- NUnitThe longest-serving .NET test framework — its constraint-based assertion model, attribute set, and the shared-instance behaviour you have to work with.
- MSTestMicrosoft's own .NET test framework — where it sits against xUnit and NUnit, what MSTest 3 improved, and when it is the right organisational choice.
- Writing Tests in C#The .NET testing stack — xUnit, Moq or NSubstitute, WebApplicationFactory, Testcontainers — and the dependency injection story that makes it the most testable of the four.
- MoqThe .NET mocking library in practical detail — setups, argument matchers, verification, loose versus strict behaviour, and what to use instead where it strains.
- Component TestingTesting one deployable in isolation through its real interface, with its own dependencies containerised and everything beyond its boundary stubbed.
- Running Tests in ParallelHow parallelism turns hidden coupling into failures, the shared resources that clash, and how to make a suite genuinely safe to run concurrently.
- Testing Avalonia ApplicationsAvalonia's headless test platform runs real UI tests in CI in milliseconds with no display server — the most testable desktop stack in .NET.
- Arrange-Act-AssertThe three-part shape every readable test has, why the act step should be one line, and the smells that show up when a test will not fit the pattern.