Articles
xUnit Interview Questions: Shared Context, Async Cleanup, and Parallel Collections
Prepare for xUnit interviews with fixture lifetimes, awaited async cleanup, isolated database tests, and version-specific rules for parallel collections.
The hardest xUnit interview questions concern what a test shares, when that shared resource becomes usable, and what another test can change while it runs. A fresh test-class object does not imply a fresh database. A collection fixture does not automatically reset records. A wider fixture lifetime does not make an object thread-safe.
Use three separate decisions: lifetime, scheduling, and state isolation. Explain who owns a resource, which tests may overlap, and how each test leaves its environment. That reasoning is more useful than memorizing fixture interfaces independently.
Practice the underlying testing judgment with Write good tests and define integration tests. Below, an original invoice/search example exposes the difference between shared infrastructure and shared mutable data.
Start with the xUnit version and runner
Official version boundary: xUnit’s v3 4.0 release, published August 14, 2026, introduced full test parallelization. Current parallelism documentation distinguishes none, collections, and, for v3 4.0+, all. The default remains collections.
This means “tests in the same collection always run serially” needs a mode qualifier. That is true in collections mode; it is not a blanket rule for all supported configurations. Establish framework and runner versions before recommending a configuration change.
Evidence boundary: Technical behavior here comes from official documentation and v3 4.0.1 API references. Our examples are original practice, not company-specific interview reports. The C# excerpts were reviewed against those references but not compiled in this environment. Separate executable SQLite and asynchronous model checks validate the example’s state assumptions, not xUnit itself.
Also distinguish the framework’s scheduling inside one assembly from the runner’s scheduling of several assemblies or processes. If two CI jobs use the same database, a setting inside one assembly does not coordinate those jobs.
How many objects does each fixture scope create?
The shared-context documentation describes a new test-class instance for each test, class fixtures shared within a class, collection fixtures shared across a collection, and v3 assembly fixtures shared across the assembly. The following counts are an original worked example.
Assume InvoiceTests and SearchTests each contain two ordinary facts, with no additional tests or theory rows. Both classes belong to one collection in one assembly. For the class-fixture comparison, each class independently declares the fixture type.
| Context placement | Instances in this example |
|---|---|
| An ordinary field initialized by the test-class constructor | Four independent field instances, one in each test-class object. |
| A class fixture declared by both classes | Two fixture instances, each shared by its class’s two tests. |
| One collection fixture for both classes | One fixture instance across the four tests. |
| One v3 assembly fixture | One fixture instance across these and any other tests in that assembly. |
Now ask what the fixture actually contains. Sharing a database server endpoint can avoid expensive startup. Sharing one open connection, mutable list, or unprotected cache adds another contract: concurrent use and test interference must be understood.
The four new test-class objects can still point to the same database through one fixture. Reinitializing a field does not undo an inserted row. State in a static variable, external service, or environment variable can also outlive a test object.
Choose the narrowest scope that justifies its setup cost. If setup is cheap and state is hard to isolate, wider sharing may save little while making failures difficult to reproduce. State the trade-off for the actual resource rather than assuming “fewer fixtures” means a better suite.
Wire collection sharing without inventing fixture order
Original wiring excerpt: DbFixture is an application-owned fixture; the following code illustrates how two classes request its collection lifetime. It is not a complete database harness.
[CollectionDefinition("Billing database")]
public sealed class BillingCollection : ICollectionFixture<DbFixture>
{
}
[Collection("Billing database")]
public sealed class InvoiceTests
{
private readonly DbFixture fixture;
private readonly string scope = Guid.NewGuid().ToString("N");
public InvoiceTests(DbFixture fixture)
{
this.fixture = fixture;
}
}
[Collection("Billing database")]
public sealed class SearchTests
{
public SearchTests(DbFixture fixture) { }
}
The scope is unique to a test-class instance; it does not become an isolation mechanism until the test’s writes, reads, and cleanup consistently use it. A query that ignores the scope can still observe another test’s data.
Official API detail: In the v3 collection-fixture API, a collection fixture can depend on assembly fixture objects through its constructor. Do not repeat the outdated blanket claim that no fixture can depend on another fixture.
Nevertheless, do not infer arbitrary sibling-fixture creation order from constructor parameter order. If your harness needs “start server, create schema, then seed immutable reference data,” make that dependency sequence explicit in an owning fixture or a supported lifetime relationship. Cleanup should follow the corresponding ownership sequence.
Use async lifetime for real asynchronous work
Official API fact: In xUnit v3, IAsyncLifetime extends IAsyncDisposable; initialization uses ValueTask. The shared-context guide explains that v2 requires its full IAsyncLifetime for asynchronous cleanup, while v3 also recognizes IAsyncDisposable independently. Match the installed version’s signatures.
Do not start asynchronous setup in a constructor and assume it finishes before the first test. Blocking with .Wait() or .Result changes the problem rather than expressing the intended lifetime. Return setup completion through the framework’s asynchronous initialization contract.
Original ownership sketch: The factory and TestStore below represent application helpers. They must be implemented for a real harness; this excerpt focuses on partial setup and awaited disposal.
public sealed class DbFixture : IAsyncLifetime
{
private TestStore? store;
public async ValueTask InitializeAsync()
{
var created = await TestStoreFactory.StartAsync();
try
{
await created.CreateSchemaAsync();
store = created;
}
catch
{
await created.DisposeAsync();
throw;
}
}
public async ValueTask DisposeAsync()
{
var owned = store;
store = null;
if (owned is not null)
await owned.DisposeAsync();
}
}
The fixture accepts ownership only after schema creation succeeds. If setup fails after the factory returns, it disposes that local resource. If the factory itself fails, the factory must handle any resources it allocated before returning.
In a production harness, preserve both setup and cleanup diagnostics if disposal also fails. The short sketch does not supply a complete exception-reporting strategy. Likewise, clearing a field makes repeated sequential disposal harmless for this ownership pattern; it does not prove thread-safe concurrent disposal.
A shared database can fail even without parallel tests
Original counterexample: Invoice test A inserts an invoice for account 42. Search test B expects its own new account to have no invoices, but the search helper queries all accounts. The tests may fail in one order and pass in another even when run serially.
Sharing the collection fixture did not promise fresh records for B. A new SearchTests object did not reset the database. Moving the tests into one collection can remove overlap under collections mode, but it cannot correct an unscoped query or leftover rows.
Choose an isolation strategy whose boundaries match the system. Options include a unique database or schema, a unique test-owned tenant namespace, or a rollback transaction where every relevant write participates in that transaction. A background worker using another connection may escape a transaction owned only by the test connection.
For our example, use a unique scope in every inserted record and every query, then delete only records owned by that scope. Include the scope in diagnostic output so a failure can be connected to its data. Do not make the test’s correctness depend on being executed after another test’s cleanup.
Trace the parallel reset that destroys another test’s evidence
Original interleaving: Invoice test A and search test B use the same external database. Suppose their effective scheduling permits overlap. B begins with a global reset intended to create a clean environment.
| Event | Shared-state consequence |
|---|---|
| A inserts invoiceA under scopeA. | A expects to read back one invoice. |
| B executes a global delete before arranging its data. | A’s row disappears, although B does not own it. |
| A performs its assertion. | It sees zero rows and fails for an unrelated reason. |
| B instead deletes only scopeB. | A’s evidence remains intact. |
This is a database ownership failure, not proof that xUnit is malfunctioning. Locks around one in-memory fixture do not coordinate another process that uses the same database. Test-owned namespaces can protect data across those process boundaries when all access paths respect the namespace.
Review more than tables. Fixed filesystem paths, queue names, ports, cache keys, and process-wide settings can create similar interference. The same question applies: can one test’s setup or cleanup modify evidence another test still needs?
Explain collections mode, all mode, and configuration
The ParallelMode API defines Collections as parallel collections with serial tests inside a collection, and All as full test parallelism by default regardless of collection. None disables parallelism inside the assembly.
Our decision rule: Shared mutable resources need an explicit safety strategy. Under collections mode, co-locating relevant tests can provide a scheduling boundary. Under all mode, review supported opt-outs or make the resource safe for overlap. Neither approach automatically resets state.
The runner JSON reference documents parallelMode for v3 4.0+ and its precedence over the older parallelizeTestCollections setting. For a supported runner reading xunit.runner.json, an explicit collections-mode example is:
{
"parallelMode": "collections"
}
If you use Microsoft Testing Platform, follow its testconfig.json reference, including the xUnit section, rather than copying configuration into an unrelated file. Confirm what the actual invocation loads. A correctly spelled setting in an unused file changes nothing.
Diagnose a flaky fixture with observations
Preparation guidance: Capture framework and runner versions, effective mode, process identity, scope identifier, initialization completion, cleanup completion, and failure timestamps. Reproduce with the smallest pair of interacting tests while preserving the suspected resource boundary.
Compare isolated execution with the relevant overlapping run. If disabling overlap removes the failure, you have evidence consistent with interference, not proof of which operation caused it. Trace the writes and cleanup to identify ownership violations.
For asynchronous cleanup, require completion before releasing the resource to the next owner. Increasing an arbitrary sleep can hide the race without establishing that the cleanup finished. Use an awaited operation or an explicit completion signal instead.
Finally, report what you verified. A source-reviewed fixture is not a compiled test suite, and a local database model is not a CI reproduction. Clear limits make your diagnosis easier for an interviewer or teammate to trust.
Sources and Further Reading
- xUnit.net: Sharing context between tests
- xUnit.net: Running tests in parallel
- xUnit.net: xunit.runner.json configuration
- xUnit.net: Microsoft Testing Platform configuration
- xUnit.net v3 4.0.1: IAsyncLifetime
- xUnit.net v3 4.0.1: ICollectionFixture
- xUnit.net v3 4.0.1: ParallelMode
- xUnit.net v3 4.0.0 release notes