Skip to content

fix(tests): create the functional test clients under a lock - #119

Merged
pdevito3 merged 1 commit into
mainfrom
fix/issue-107-functional-tests-client-race
Oct 25, 2026
Merged

pdevito3 merged 1 commit into
mainfrom
fix/issue-107-functional-tests-client-race

Conversation

@pdevito3

Copy link
Copy Markdown
Owner

Why

The FunctionalTests teardown sometimes failed with AfterAssembly hook failed: Object reference not set to an instance of an object after every test passed. WebApplicationFactory adds each client to a List<HttpClient> with no lock. Each test creates a client in the TestBase constructor, and TUnit runs those constructors in parallel. Two adds at the same time can lose a client or leave a null slot, and DisposeAsync then throws on the null slot.

Closes #107

What changed

  • TestingWebApplicationFactory.CreateClient() now hides the base method and calls it under a lock. TestBase calls it through the TestingWebApplicationFactory type, so the lock applies.
  • This is the same pattern as BffApp in the Bff tests.

Scope

  • Only the functional test factory changes. No production code changes.

Blast Radius

Only the FunctionalTests project uses this factory. The lock serializes only the client creation. The runs took 9 to 21 seconds before the fix and 10 to 23 seconds after it, so the lock adds no time that these runs can show.

Verification

A temporary probe counted the CreateClient calls against the entries and null slots in the factory list at teardown. The probe is not in this PR.

Run Calls Entries Null slots Result
Before, plain runs 1-5 278 275, 277, 276, 273, 271 0 287 of 287 passed
Before, stress with 2000 extra clients 2278 2276 0 passed
Before, stress with 20000 extra clients 20278 18229 0 passed
Before, one null slot injected 278 277 1 the issue's NRE at TestingWebApplicationFactory.cs:130
After, stress with 20000 extra clients 20278 20278 0 passed
After, plain runs 1-5 278 278 0 287 of 287 passed
  • After the probe was removed, the FunctionalTests passed 287 of 287.
  • dotnet build FullstackTemplate.slnx has 0 warnings, and dotnet test --solution FullstackTemplate.slnx passed 1719 of 1719.

WebApplicationFactory adds each client to a List with no lock. The
FunctionalTests create one client in each TestBase constructor, and TUnit
runs those constructors in parallel. Two adds at the same time can lose a
client or leave a null slot, and DisposeAsync then throws in the
AfterAssembly hook.

TestingWebApplicationFactory.CreateClient now takes a lock around the base
call, the same pattern as BffApp in the Bff tests.

Closes #107
@pdevito3

Copy link
Copy Markdown
Owner Author

Review gate result: no high or medium finding. These low findings stay open:

  • Only CreateClient() takes the lock. A future call to CreateClient(options), to CreateDefaultClient, or to CreateClient() through a WebApplicationFactory<Program> reference would skip it. The base class has no virtual hook before it adds the client, so method hiding is the only option, and BffApp has the same limit. No caller does this today.
  • No test asserts that the client list has one entry for each call. A reliable test of this race would be heavy and slow. The probe numbers in the description are the proof.

The review also found a separate race on the in-memory telemetry lists. It is filed as #118.

@pdevito3
pdevito3 merged commit 143ca8b into main Oct 25, 2026
9 checks passed
@pdevito3
pdevito3 deleted the fix/issue-107-functional-tests-client-race branch October 25, 2026 10:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FunctionalTests teardown sometimes fails with a NullReferenceException in WebApplicationFactory.DisposeAsync

1 participant