Skip to content
End To End Tester

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

csharp
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.

csharp
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

csharp
// 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.

csharp
// Opt a set of classes into running sequentially by naming a collection.
[Collection("Sequential")]
public class MigrationTests { }
xml
<!-- 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:

csharp
// 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

csharp
// 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?