Repository navigation
Tests: EF Core integration tests for scope per dispatch, and mutation-testing gaps - #5
Merged
Merged
Conversation
New project tests/Switchboard.IntegrationTests, added to Switchboard.slnx so CI's plain dotnet restore/build/test runs it. It is kept apart from Switchboard.Tests so EF Core never lifts that project's pinned Microsoft.Extensions.DependencyInjection floor. EF Core Sqlite 8.0.32 / 9.0.21 / 10.0.13 per framework, against a shared-cache in-memory database. A Blazor circuit is simulated as one long-lived scope with a scoped AddDbContext and the ISender resolved from it. A DbCommandInterceptor gate holds one command just before it executes, so a dispatch is provably inside EF's critical section when the next one starts, with no timing involved. Covered: - Without scope per dispatch, overlapping sends share the circuit's DbContext and EF throws "A second operation was started on this context instance". With it, each gets its own context and both rows are saved. - Nested sends and published events share the outer DbContext and transaction; rolling back the outer transaction discards the nested write. - Streaming: a write sent inside await foreach gets its own context and the stream's scope ends with the enumeration. A save still in flight when the shared-context stream reads its next row makes EF throw; with scope per dispatch it works, and abandoning the stream disposes its scope. - The UseScopePerDispatch callback (async and sync) carries the circuit's user into the dispatch, including across overlapping dispatches from two circuits; without it the dispatch scope has no user. Finding: a write awaited inside await foreach over a shared-context AsAsyncEnumerable() does not fail on SQLite. EF holds its concurrency check only during each MoveNextAsync, and Microsoft.Data.Sqlite allows a command while a reader is open. A test documents that behavior as observed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qh4oHys2D2TLycvt3yKVtX
`dotnet tool restore && dotnet stryker` mutates src/Switchboard against the test project on net10.0 (Release) and writes progress, cleartext, HTML and JSON reports to StrykerOutput/, which is now git-ignored. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qh4oHys2D2TLycvt3yKVtX
Stryker.NET left 68 mutants alive (47 survived, 21 without coverage); these tests kill the ones that stand for behavior that could regress unnoticed. Validation (ValidationGapTests): - a void request without a handler is reported; - abstract, interface and open generic request types are not reported; - the void-request skip check: five names then "and N more", an exact message for a single skipped request, class/struct/new() constraints on TRequest and TResponse, a constraint on the behavior's own type parameter, and open generic services that are not pipeline behaviors; - ValidateSwitchboard names its null argument. Dispatch (DispatchGapTests): - Send of a void request awaits an async handler and surfaces its failure; - a failing notification handler fails Publish and stops later handlers; - Publish of a non-notification names its parameter; - a stream request that implements another interface still streams; - a request nested in a generic type dispatches and gets a readable name; - UseScopePerDispatch and AddSwitchboard reject null arguments up front. Telemetry (TelemetryGapTests): - histogram units are seconds; - a failing notification handler's span carries its tags, error status and the OpenTelemetry exception event; - cancelled requests and streams are tagged but not marked as errors; - a traced stream still passes the WithCancellation token to the handler (found by hand-mutating ObserveStream, which Stryker's safe mode skips). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qh4oHys2D2TLycvt3yKVtX
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Tests only. Nothing under
src/changes, and no existing test is modified. There are two independent pieces, one per commit group.1. EF Core integration tests (
tests/Switchboard.IntegrationTests/, new project)Until now the Blazor Server promise in the README was tested only with a stand-in
ScopeMarker. These tests prove it against real EF Core (SQLite, in memory) on EF 8, 9 and 10.Why a separate project:
Switchboard.TestspinsMicrosoft.Extensions.DependencyInjectionat 8.0.0 / 9.0.0 / 10.0.0 to test the floor the library supports. EF Core would drag that upward, so EF lives in its own project, added toSwitchboard.slnxso CI's plaindotnet testruns it. Package versions per framework:Microsoft.EntityFrameworkCore.Sqlite8.0.32 (net8.0)Deterministic, not timing-based: Microsoft.Data.Sqlite completes async calls synchronously, so dispatches never overlap on their own. A
DbCommandInterceptorholds a chosen command on a gate (CommandGate), so one dispatch is provably inside EF's critical section while the test starts another. There is noTask.Delay; the only timeout is a 30 s hang guard.What it proves (14 tests):
UseScopePerDispatch(), two overlappingSends from one long-lived "circuit" scope share its DbContext. EF throws the real error: "A second operation was started on this context instance before a previous operation completed." It is the same message on EF 8, 9 and 10.Send(and aPublishof a domain event) from a handler shares the outer DbContext and its transaction: when the outer handler rolls back, nothing is persisted.await foreacheach get their own context.MoveNextAsync.UseScopePerDispatch(callback)overloads carry the circuit's current user into the dispatch scope, even with two circuits overlapping. A bareUseScopePerDispatch()doesn't, which shows why the callback exists.One finding, not a bug: a stream handler reading rows while the consumer sequentially writes inside the loop on the shared context does not fail on SQLite. EF only guards each
MoveNextAsync, and SQLite allows a command while a reader is open. It would likely fail on SQL Server without MARS ("There is already an open DataReader…"), but that can't be tested here. The README doesn't claim this case fails, so no docs change. One test documents the actual behavior.2. Tests for gaps found by mutation testing (Stryker.NET)
Mutation testing makes small deliberate changes to the code and checks whether any test notices. The score went from 77.85% to 92.18% with 24 new tests:
ValidationGapTests.cs:class/struct/new()/ self-referencing constraint branchesDispatchGapTests.cs:TelemetryGapTests.cs:WithCancellationtokenThe 24 survivors left alone fall into five groups:
>= 5boundaryStryker can't instrument
SwitchboardTelemetry.ObserveStream: its own rewrite fails to compile with CS0165. That method was therefore mutation-tested by hand with 16 mutants, and the one survivor is now killed. No bugs were found.To re-run, the commit adds Stryker as a local tool (
.config/dotnet-tools.json,stryker-config.json;StrykerOutput/is git-ignored):dotnet tool restore && dotnet stryker. It is not wired into CI. It takes a while, which is better for an occasional local run.Verification
dotnet build -c Releasegives 0 warnings.dotnet test -c Releasepasses 155 unit + 14 integration tests on net8.0, net9.0 and net10.0, in 3 consecutive full runs.Generated by Claude Code