NUnit
02 / 02

Parallelization & Advanced Patterns

Parallelization & Advanced Patterns

Parallel Execution

// AssemblyInfo.cs or any file in the test project
[assembly: Parallelizable(ParallelScope.Fixtures)]
[assembly: LevelOfParallelism(4)]

// Combinatorial explosion vs Pairwise coverage
[Test]
public void Render_AcrossBrowsersAndResolutions(
    [Values("Chrome", "Firefox", "Safari")] string browser,
    [Values(720, 1080, 1440)] int height)
{
    // [Combinatorial] (default) = 3 * 3 = 9 runs
    // [Pairwise] on the method instead = fewer runs, still covers each pair once
}

// Shared mutable state is a hazard once fixtures run in parallel —
// scope resources per-instance, not static, when [Parallelizable] is on.
[OneTimeSetUp]
public void StartTestServer()
{
    _server = new InMemoryTestServer();  // instance field, not static
}

TestContext & Diagnostics

[TearDown]
public void SaveScreenshotOnFailure()
{
    var outcome = TestContext.CurrentContext.Result.Outcome;
    if (outcome.Status == TestStatus.Failed)
    {
        var path = Path.Combine(TestContext.CurrentContext.WorkDirectory, "failure.png");
        _driver.TakeScreenshot(path);
        TestContext.WriteLine($"Saved failure screenshot to {path}");
    }
}

// Retry vs Repeat — very different intent
[Retry(3)]   // keeps the run a pass if ANY attempt succeeds — use sparingly, can mask flakiness
public void FlakyNetworkCall() { ... }

[Repeat(5)]  // requires EVERY repetition to pass — good for surfacing intermittent bugs
public void RaceConditionCandidate() { ... }

[Timeout(2000)]  // fails if the test doesn't finish in time; runs on a separate thread
public void MustCompleteQuickly() { ... }

Setup Fixtures & Custom Constraints

// Runs once for every fixture in this namespace — good for expensive shared
// infrastructure like a test container, above per-fixture [OneTimeSetUp].
namespace MyApp.Tests.Integration
{
    [SetUpFixture]
    public class NamespaceSetup
    {
        [OneTimeSetUp]
        public void StartContainer() => TestDatabase.Start();

        [OneTimeTearDown]
        public void StopContainer() => TestDatabase.Stop();
    }
}

// Custom, reusable constraint
public class ValidEmailConstraint : Constraint
{
    public override ConstraintResult ApplyTo<TActual>(TActual actual)
    {
        bool isValid = actual is string s && s.Contains('@');
        return new ConstraintResult(this, actual, isValid);
    }
}

public static class Is2
{
    public static ValidEmailConstraint ValidEmail() => new ValidEmailConstraint();
}

// Assert.That(user.Email, Is2.ValidEmail());

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free