cswork / libraryOpen practice workspace

Articles

NUnit Interview Questions: Fixture Lifetimes, Parallel Tests, and Reliable Assertions

Answer NUnit interview questions with fixture lifetime traces, safe parallel isolation, awaited cleanup, and assertions that detect plausible wrong results.

NUnit interview questions about fixtures and parallel execution are really questions about who owns mutable state, when work finishes, and what an assertion proves. Adding [SetUp] does not automatically make parallel tests independent. More assertions help only when they reject meaningful violations of the result contract.

This guide traces two price-quote tests that overwrite shared state, then repairs their ownership and challenges their assertions.

Official facts below come from NUnit documentation checked on October 6, 2026. The quote fixture, schedules, and review exercises are original preparation examples, not reported questions from an employer. Local Python checks execute the state and assertion models; the C# examples are source-reviewed illustrations because a .NET test runtime is unavailable in this workspace.

What is a fixture lifetime, and what does it isolate?

NUnit's FixtureLifeCycle documentation distinguishes SingleInstance, the usual default, from InstancePerTestCase. The latter constructs a new fixture instance for every test and was introduced in NUnit 3.13.

That choice defines fixture-instance ownership. It does not create a private process, database, filesystem, or singleton for every test. Instance fields can be independent while a static dictionary or one shared database row remains common to the entire run.

For our example, a quote test uses a mutable discount percentage. Test A expects a 10% discount on 100 USD, producing 90 USD. Test B expects a 20% discount on 100 USD, producing 80 USD. Each answer is straightforward when calculated alone.

The question changes when both tests reference the same mutable fixture or the same calculator stored inside it. The arithmetic is still correct, but A can read the discount most recently written by B. That produces a plausible price, which makes a weak assertion especially dangerous.

Before selecting an attribute, draw the ownership boundary. Is the percentage on a fixture instance, a static service, an injected shared calculator, or an external configuration record? A fixture-lifetime attribute addresses the first boundary; inspect the other three independently.

Why doesn't SetUp eliminate a parallel race?

NUnit's SetUp reference describes setup before each test. A setup method can establish initial conditions; it does not make the following test body an exclusive transaction.

Here is an original schedule for a shared mutable discount field. A waits for an asynchronous seed operation before reading the field for its calculation.

StepTest ATest BShared percentage
1SetUp resets the fieldNot started0
2Writes 10, awaits seedingNot started10
3Still awaitingSetUp resets the field0
4Still awaitingWrites 2020
5Resumes, calculates its quoteContinues20

A receives 80 instead of 90. The failure follows from shared ownership and an allowed schedule; it does not require a defective percentage formula.

A different ordering may let both tests pass while leaving the ownership defect in place. Therefore, “it passed when rerun” is not evidence that the shared-state defect disappeared. Capture the writes, the await boundary, and the later read. You can then explain the race without relying on vague descriptions such as “parallel tests are flaky.”

This schedule also depends on when the value is read. If A copied 10 into a local variable before awaiting and used that immutable local afterward, B's field overwrite would not change A's chosen percentage. Precision about the read matters as much as precision about the write.

How do you preserve parallelism while repairing the fixture?

For these quotes, pass each discount percentage as a test-local input before introducing mutable fixture state. Pass the percentage as a local argument when the production interface permits it. A local value usually expresses this exercise's input better than changing global configuration.

If the test genuinely needs mutable instance state, InstancePerTestCase gives each test a separate fixture. In the following illustrative fixture, each test case receives its own calculator. The deliberate yield makes the separation visible; it is not a timing-based test of thread safety.

using NUnit.Framework;
using System.Threading.Tasks;

[TestFixture]
[FixtureLifeCycle(LifeCycle.InstancePerTestCase)]
[Parallelizable(ParallelScope.All)]
public sealed class QuoteTests
{
    private readonly QuoteCalculator _calculator = new();

    [TestCase(10, 90)]
    [TestCase(20, 80)]
    public async Task DiscountIsIndependent(int percent, int expected)
    {
        _calculator.Percent = percent;
        await Task.Yield();
        Assert.That(_calculator.Quote(100m), Is.EqualTo((decimal)expected));
    }
}

public sealed class QuoteCalculator
{
    public int Percent { get; set; }
    public decimal Quote(decimal price) => price * (100 - Percent) / 100m;
}

If both test cases use one static calculator, the per-test fixture instances still share its mutable percentage. So would having every new calculator wrap the same mutable singleton.

For a database integration fixture, give each test its own logical record keys or isolated database scope. Separate object instances that update the same quote_config row remain competitors. Decide whether the scarce resource needs controlled serialization or whether it can be partitioned safely.

The goal is an ownership repair with an explanation. Disabling parallelism across the entire suite may hide the problem while imposing unnecessary cost on unrelated independent tests.

What does ParallelScope actually promise?

The official Parallelizable reference describes eligibility for parallel execution and makes the test author responsible for thread safety. Self, Children, Fixtures, and All apply at different structural levels; some combinations are invalid on particular elements.

An attribute is therefore a scheduling declaration, not proof that operations overlap in a particular run. Available workers, the runner, suite structure, and other settings influence what actually executes concurrently. A reassuring serialized local run cannot rule out the failing concurrent schedule.

For an interview, name the desired unit of independence. Do you want fixtures to run beside other fixtures, or the children of one fixture to run beside one another? Then show why each shared resource remains safe under that scope.

Our quote example deliberately permits the test cases to run in parallel. Its repair must hold if both cases suspend and resume in either order. A correctness argument based on “test A usually finishes first” does not meet that requirement.

Use targeted serialization when a resource truly cannot be isolated. Document the reason, such as a device accepting only one command stream. Do not label every integration test inherently nonparallel without inspecting its resource ownership.

How should one-time setup and asynchronous cleanup behave?

Under InstancePerTestCase, NUnit requires OneTimeSetUp and OneTimeTearDown methods to be static; they run once rather than initializing each new fixture's instance fields. That constraint belongs to the lifecycle choice described above.

The OneTimeSetUp documentation also distinguishes one-time initialization from setup for each test. Think carefully before putting a mutable calculator in that shared initialization. It would undo the ownership you were trying to obtain.

The Test attribute documentation says NUnit awaits asynchronous test methods returning Task or Task<T>. Returning a task gives the framework an observable completion boundary. Starting background work and returning before it finishes breaks that boundary.

For our quote service, await creation of the test record, await the operation under test, and await cleanup of that record. A test that starts seeding without awaiting it can measure an uninitialized service. Cleanup that continues after the test ends can delete another test's reused record.

NUnit's TearDown documentation supports asynchronous teardown and describes its setup-related execution conditions. Treat cleanup as fallible code. If resource acquisition fails halfway through setup, design local ownership so the already-acquired resource can be released; do not assume a later teardown always compensates for incomplete setup.

Avoid using sleeps as evidence that cleanup finished. Await the operation or an explicit synchronization signal. Time passing does not identify which resource was created, who owns it, or whether its deletion succeeded.

Which assertions reject plausible wrong quotes?

Consider three implementations returning one line item. The correct result is line L1, currency USD, total 90. One wrong result contains L1, USD, total 80. Another contains L9, EUR, total 90.

All three return one item. An assertion checking only item count passes all three, even though two answers violate the contract. Checking only total rejects the first wrong result but still accepts the wrong currency and line identity.

ResultCount equals 1Total equals 90Currency and line ID correct
L1, USD, 90PassPassPass
L1, USD, 80PassFailPass
L9, EUR, 90PassPassFail

Choose assertions from observable business behavior, rather than from the fields convenient to inspect. For this exercise, assert the exact total, currency, and line identity. Assert order only if the interface promises order; otherwise, compare the intended membership without inventing a sorting requirement.

Our use of decimal models monetary arithmetic. A test using approximate floating-point comparison would need a justified tolerance and a matching production contract. A broad tolerance can conceal a real discount defect just as a count-only assertion can.

NUnit's multiple-assert documentation supports collecting related assertion failures. Assert.EnterMultipleScope() is available from NUnit 4.2; earlier versions use Assert.Multiple. Keep that version boundary explicit when discussing syntax.

Use multiple-assert scopes for related quote fields when the installed NUnit version supports the chosen syntax. First ensure the result exists: an unexpected exception inside a multiple-assert scope still stops normal execution. Collecting failures does not make invalid test code safe.

How do you demonstrate that the repair is meaningful?

Replay the exact failing schedule with two separate calculator states. A retains 10 while B retains 20, so their totals remain 90 and 80. Then deliberately substitute a static shared state and show that the original counterexample returns.

Challenge the assertions separately. Feed them the wrong-price and wrong-identity results from the table. If the test still passes, its acceptance criteria need repair even if fixture isolation is now correct.

The accompanying local Python model checks that schedule and these counterfeit results. It verifies the reasoning assets, not NUnit scheduling, adapter behavior, or compilation of the C# illustration. For an actual repository repair, run the affected suite using its installed NUnit framework and runner, including the relevant parallel configuration.

A useful interview explanation ends with two independent pieces of evidence: the ownership model prevents the overwrite, and the assertions reject meaningful wrong results. Neither piece replaces the other.

What evidence should a failure report contain?

Record the test-case inputs, the resource identifier, and the value observed at the decisive read. In our schedule, reporting only expected 90 and actual 80 leaves several explanations open. Recording that A read percentage 20 after B wrote it connects the failure to shared ownership.

Keep test-owned diagnostics separate from shared mutable logs when those logs can interleave. A unique test-case identifier makes events attributable without depending on their console order. For an external record, record its logical key so an unexpected cleanup can be traced to the test that owned it.

After a repair, preserve the original counterexample as an explanation of the change. Repeated passing runs add operational evidence, but the ownership argument should already explain why the specific overwrite is no longer possible. If a remaining external resource can still collide, identify that separate boundary instead of declaring the whole suite isolated.

Practice state ownership and test strength

The Java and Python questions transfer testing reasoning; their framework APIs should not be presented as NUnit APIs.

QuestionPractice target
Debug and Fix Failing Unit Tests in JavaExplain a failing unit test before changing expected output.
Design employee and company classes with testsDerive assertions from object behavior and contracts.
Debug a Concurrent Job SchedulerTrace concurrent writes, waits, and reads.
Implement a Python test harnessMake execution and cleanup observable in a test harness.
Refactor code and enforce robustnessPreserve behavior while repairing structure.

Return to Debug a Concurrent Job Scheduler and write a precise ownership explanation alongside the code. Identify one schedule that breaks the old design and one observable wrong result that the new assertions reject.

Sources and Further Reading