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.
[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.
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.
[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
// 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
[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?
Related topics
- xUnit.netThe .NET runner with a fresh instance per test — Facts, Theories, fixtures, parallelism and the design opinions baked into it.
- 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.
- 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.