Skip to content
End To End Tester

NUnit

The longest-serving .NET test framework — its constraint-based assertion model, attribute set, and the shared-instance behaviour you have to work with.

1 min read · updated 19 September 2026

NUnit is the oldest of the .NET test frameworks and still one of the most capable. Its assertion model and parameterisation options are richer than xUnit's; its instance model is the thing to be careful about.

#The shared instance

One instance of the fixture class serves every test in it. A field mutated by one test is visible to the next, in whatever order the runner chose.

csharp
[TestFixture]
public class BasketTests
{
    private Basket _basket = null!;

    // Runs before every test. Put setup HERE, not in a field initialiser,
    // or two tests will share one Basket.
    [SetUp]
    public void SetUp() => _basket = new Basket();

    [TearDown]
    public void TearDown() { /* per-test cleanup */ }

    [OneTimeSetUp]
    public void OnceBeforeAll() { /* expensive shared setup */ }

    [Test]
    public void An_empty_basket_costs_nothing() =>
        Assert.That(_basket.TotalCents, Is.Zero);
}

#The constraint model

NUnit's Assert.That(actual, Is.Something) reads well and composes, which is its main ergonomic advantage.

csharp
Assert.That(result.DiscountCents, Is.EqualTo(2_400));
Assert.That(order.Lines, Has.Exactly(1).Matches<OrderLine>(l => l.Sku == "book-1"));
Assert.That(total, Is.InRange(1_000, 2_000));
Assert.That(names, Is.EquivalentTo(new[] { "a", "b" }));          // order-insensitive
Assert.That(price, Is.EqualTo(12.34).Within(0.001));              // floats
Assert.That(() => service.Reserve(order), Throws.TypeOf<InsufficientStockException>());

// Multiple: report every failure, not just the first.
Assert.Multiple(() =>
{
    Assert.That(order.Status, Is.EqualTo(OrderStatus.Paid));
    Assert.That(order.TotalCents, Is.EqualTo(2_795));
    Assert.That(order.ShippedAt, Is.Null);
});

Assert.Multiple is genuinely useful and has no direct xUnit equivalent: when three facets of one outcome are wrong, you see all three rather than fixing them one build at a time.

#Parameterisation

NUnit has the widest set of options of any .NET framework.

csharp
[TestCase(12_000, ExpectedResult = 2_400)]
[TestCase(10_000, ExpectedResult = 2_000)]
[TestCase(9_999, ExpectedResult = 0)]
public int Discount_applies_at_or_above_the_threshold(int subtotal) =>
    Pricing.ApplyDiscount(new Order(subtotal), Policy).DiscountCents;

// Cartesian product: every combination of the two, 6 tests from 5 values.
[Test]
public void Shipping_is_priced_for_every_tier_and_region(
    [Values(CustomerTier.Standard, CustomerTier.Gold, CustomerTier.Platinum)] CustomerTier tier,
    [Values("UK", "EU")] string region)
{
    Assert.That(Shipping.For(tier, region).Cents, Is.GreaterThanOrEqualTo(0));
}

// Data from a method or class
[TestCaseSource(nameof(ShippingCases))]
public void Shipping_cases(CustomerTier tier, int expected) { }

// Property-ish testing: 100 random values in a range
[Test]
public void Never_discounts_below_zero([Random(0, 100_000, 100)] int subtotal) =>
    Assert.That(Pricing.ApplyDiscount(new Order(subtotal), Policy).DiscountCents,
                Is.GreaterThanOrEqualTo(0));

ExpectedResult is a neat touch — the test returns the value and NUnit does the assertion, which keeps arrange-act-assert to two lines.

#Parallelism

csharp
// AssemblyInfo.cs — fixtures in parallel, tests within a fixture sequential.
[assembly: Parallelizable(ParallelScope.Fixtures)]
[assembly: LevelOfParallelism(4)]

// Per-fixture override, where tests genuinely are independent.
[TestFixture]
[Parallelizable(ParallelScope.All)]
public class PureCalculationTests { }

// And opt out where they are not.
[TestFixture]
[NonParallelizable]
public class MigrationTests { }

Because instances are shared within a fixture, ParallelScope.All is only safe when the fixture holds no mutable state. That is the practical cost of the shared-instance model — see parallel test execution.

#Useful attributes

csharp
[Category("Integration")]                    // dotnet test --filter TestCategory=Integration
[Explicit("Run by hand: hits the sandbox")]  // excluded unless named
[Retry(2)]                                   // use sparingly, and see /practices/flaky-tests
[Timeout(5_000)]
[Order(1)]                                   // avoid: an ordered suite is a coupled suite
[Ignore("ENG-4412, owner @sam, review 2026-10-15")]

[Retry] deserves the same warning as everywhere else: it converts a diagnosable failure into an invisible one. If you use it, count it.

#Where it fits

NUnit is a good choice for a team that already uses it, for suites that benefit from its richer parameterisation, and for cases where Assert.Multiple and the constraint model genuinely improve readability.

For a new project with no history, xUnit is the more common default and its instance model removes a real class of mistake. MSTest is the third option and mostly chosen for organisational reasons.

Common questions

Should I use NUnit or xUnit for a new project?
xUnit is the more common default for new .NET projects, chiefly because its fresh-instance-per-test model removes a class of shared-state bug. NUnit has a richer assertion and parameterisation set and remains an entirely reasonable choice, especially for a team that already knows it.
Does NUnit create a new test class instance per test?
No. One instance is shared by all tests in the class, which means a field mutated by one test is visible to the next. Set state up in [SetUp] rather than in field initialisers or the constructor.
How do I run NUnit tests in parallel?
Add [assembly: Parallelizable(ParallelScope.Fixtures)] and set LevelOfParallelism. Because instances are shared within a fixture, parallelising at the fixture level is the safe default.

Runnable samples for this page

last test results ↗
  • C#dotnet/Tests.NUnit/tools/nunit

Working tests, not fragments — they run in CI on every push to 8exgh/endtoendtester-samples.

Was this page useful?