ScoreBoard Unit Tests Documentation¶
Related Linear Issue: LBS-621 - ScoreBoard Aggregate with Cricket Scoring
This document describes how the ScoreBoard aggregate test suite is organised and how to run it. The aggregate is sport-aware: it tracks a shared "details" core (teams, sport, status, period, match summary) and then sport-specific state for Cricket and Rugby League. The tests mirror that split.
Aggregate commands and events¶
The aggregate is a single partial class ScoreBoardAggregate : AggregateRoot<ScoreBoardId> spread over three source files:
src/Domain/LBS.Domain.Sport/Scoreboard/ScoreBoardAggregate.cs- shared core (details, match summary, self-healing helper).src/Domain/LBS.Domain.Sport/Scoreboard/ScoreBoardAggregate.Cricket.cs- all Cricket behaviour.src/Domain/LBS.Domain.Sport/Scoreboard/ScoreBoardAggregate.RugbyLeague.cs- all Rugby League behaviour.
Commands live under src/Domain/LBS.Domain.Sport/Scoreboard/Commands/ and events under src/Domain/LBS.Domain.Sport/Scoreboard/Events/ (each with Cricket and RugbyLeague sub-folders).
| Command | Event(s) raised | Source |
|---|---|---|
SetScoreBoardDetailsCommand |
ScoreBoardDetailsSetEvent |
ScoreBoardAggregate.cs |
SetMatchSummaryCommand |
MatchSummarySetEvent |
ScoreBoardAggregate.cs |
SetCricketScoreCommand |
CricketScoreSetEvent (and SportingEventCompletedEvent when status becomes Completed) |
ScoreBoardAggregate.Cricket.cs |
SetCricketTossOutcomeCommand |
CricketTossOutcomeSetEvent |
ScoreBoardAggregate.Cricket.cs |
SetCricketLineupCommand |
CricketLineupSetEvent |
ScoreBoardAggregate.Cricket.cs |
UpdateCricketBallByBallDetailsCommand |
CricketBallByBallDetailsSetEvent, CricketBallByBallDetailsRemovedEvent |
ScoreBoardAggregate.Cricket.cs |
SetCricketBatterDismissalCommand |
CricketBatterDismissalSetEvent |
ScoreBoardAggregate.Cricket.cs |
RemoveCricketBallByBallDetailsCommand |
CricketBallByBallDetailsRemovedEvent |
ScoreBoardAggregate.Cricket.cs |
SetRugbyLeagueScoreCommand |
RugbyLeagueScoreSetEvent |
ScoreBoardAggregate.RugbyLeague.cs |
SetRugbyLeagueParticipantStatsCommand |
RugbyLeagueParticipantStatsSetEvent |
ScoreBoardAggregate.RugbyLeague.cs |
SetRugbyLeagueLineupCommand |
RugbyLeagueLineupSetEvent |
ScoreBoardAggregate.RugbyLeague.cs |
SetRugbyLeagueMatchClockCommand |
RugbyLeagueMatchClockSetEvent |
ScoreBoardAggregate.RugbyLeague.cs |
Two events are cross-cutting rather than tied to a single command:
ScoreBoardDataRequestedEvent- raised by the self-healing helperTryRaiseDataRequestedEvent. When a sport command arrives before the scoreboard details have been set (requiresAdditionalData == trueand no request has been raised yet), the aggregate emits this event so an integrator can backfill team/metadata. It is applied (hasRequestedData = true) so it only fires once.SportingEventCompletedEvent- raised fromSetCricketScoreCommandwhen the incoming status isMatchStatuses.Completedand the aggregate has not already recorded completion. It is applied purely to drive integrator triggers (isSportingEventCompleted = true), which also short-circuits later score updates.
Idempotency is enforced inside every Execute method: a command that carries no change (same teams, same summary, same innings/periods, same clock state, same participant stats, etc.) returns without raising an event.
Test file layout¶
All ScoreBoard tests live under src/Tests/LBS.UnitTests/Scoreboard/. The aggregate tests are themselves a single partial class ScoreBoardAggregateTests split across four files, in the same shape as the production aggregate. The integrator and projection-builder tests are separate, non-partial classes.
| File | Class | Coverage |
|---|---|---|
ScoreBoardAggregateTests.cs |
ScoreBoardAggregateTests (partial) |
Shared core: test fixture/ctor (command + event registration via DomainTypeMappings), SetScoreBoardDetailsCommand, SetMatchSummaryCommand, and SportingEventCompletedEvent behaviour. |
ScoreBoardAggregateTests.Cricket.cs |
ScoreBoardAggregateTests (partial) |
Cricket: SetCricketScoreCommand, SetCricketTossOutcomeCommand (+ CricketTossOutcomeSetEvent apply/state tests), and UpdateCricketBallByBallDetailsCommand (behaviour + validation). Also defines the shared Cricket team-id fixtures. |
ScoreBoardAggregateTests.RugbyLeague.cs |
ScoreBoardAggregateTests (partial) |
Rugby League: SetRugbyLeagueScoreCommand and SetRugbyLeagueParticipantStatsCommand (behaviour + validation), plus Rugby League helper builders. |
ScoreBoardAggregateTests.RugbyLeagueMatchClock.cs |
ScoreBoardAggregateTests (partial) |
Rugby League match clock: SetRugbyLeagueMatchClockCommand (tiers, period/state transitions, idempotency, validation). |
ScoreBoardIntegratorTests.cs |
ScoreBoardIntegratorTests |
ScoreBoardIntegrator: proactive creation on SportingEventCreatedEvent, reactive backfill on ScoreBoardDataRequestedEvent, and logging. Uses NSubstitute for ICommandExecutor and IDocumentStore. |
ScoreBoardContractBuilderTests.cs |
ScoreBoardContractBuilderTests |
ScoreBoardContractBuilder read-model projection: applying ScoreBoardDetailsSetEvent, CricketScoreSetEvent, CricketTossOutcomeSetEvent, MatchSummarySetEvent, and Rugby League events to build/update the contract. |
Because the four ScoreBoardAggregateTests.*.cs files compile into one class, they share the constructor in ScoreBoardAggregateTests.cs (which registers every command/event with DomainTypeMappings and builds a CommandExecutor over an InMemoryDomainEventStore) and the static team-id fields. Each file scopes its tests with #region blocks named after the command under test.
Shared core (ScoreBoardAggregateTests.cs)¶
SetScoreBoardDetailsCommandtests - first-time details set raisesScoreBoardDetailsSetEvent; details arriving after a score has already created a partial scoreboard backfills metadata; null teams throwInvalidCommandException; identical details are idempotent; different teams raise a new event.SetMatchSummaryCommandtests - valid summary raisesMatchSummarySetEvent; same summary / different case / extra whitespace are all treated as no-change; a genuinely different summary raises a new event; emptySportingEventId, null/empty/whitespace summary all throw with specific messages; repeated updates apply the latest value.SportingEventCompletedEventtests - aSetCricketScoreCommandwithMatchStatuses.CompletedraisesCricketScoreSetEventfollowed bySportingEventCompletedEvent; the completed event fires only once across status transitions; event ordering (ScoreBoardDataRequestedEvent, thenCricketScoreSetEvent, thenSportingEventCompletedEvent) is asserted; a non-completed status does not raise it.
Cricket (ScoreBoardAggregateTests.Cricket.cs)¶
SetCricketScoreCommandtests - on a brand-new aggregate, raisesScoreBoardDataRequestedEventplusCricketScoreSetEvent(self-healing); after details are set, raises onlyCricketScoreSetEvent; identical scores are idempotent; wrong sport, negative runs, and emptySportingEventIdthrow.SetCricketTossOutcomeCommandtests - valid toss raises data-requested plusCricketTossOutcomeSetEventwith a generated toss summary; same toss is idempotent; different toss raises a new event; full validation matrix (wrong sport, empty ids/names, whitespace) with asserted messages. (One scenario - executing the toss only while pre-match - is[Fact(Skip = ...)]pending that rule.)CricketTossOutcomeSetEventapply/state tests - confirm state is applied correctly on new and existing aggregates, across multiple sequential applies, and that the sport is set to Cricket.UpdateCricketBallByBallDetailsCommandtests - adding balls to an in-progress match succeeds and raisesCricketBallByBallDetailsSetEvent; plus a validation block covering emptySportingEventId, empty ball lists, and ball-numbering rules.
Rugby League (ScoreBoardAggregateTests.RugbyLeague.cs)¶
SetRugbyLeagueScoreCommandtests - first score, score/status/period updates, idempotency on identical scores, multi-period storage; on a new aggregate it raisesScoreBoardDataRequestedEventplusRugbyLeagueScoreSetEvent; validation covers wrong sport, emptySportingEventId, negative points/tries, and empty periods.SetRugbyLeagueParticipantStatsCommandtests - first stats, updates, idempotency, multiple participants tracked separately, full stat-field round-tripping, on-field toggling, and validation (wrong sport, emptySportingEventId, empty participant id, missing name); also verifies an uninitialised aggregate adopts Rugby League.
Rugby League match clock (ScoreBoardAggregateTests.RugbyLeagueMatchClock.cs)¶
SetRugbyLeagueMatchClockCommandtests - full-tier in-play, minimal-tier (null optional fields), pre-match, half-time, extra-time and post-match transitions; idempotency on identical state vs. raising on clock-value / period / duration / match-state changes; data-source propagation and the rule that the data source cannot change mid-match; on a new aggregate it raises data-requested plusRugbyLeagueMatchClockSetEvent; validation covers wrong sport, emptySportingEventId, invalid match state, invalid source tier, invalid clock direction, negative clock value, and empty/unknown data source.
Integrator (ScoreBoardIntegratorTests.cs)¶
ScoreBoardIntegrator is exercised with NSubstitute mocks. Coverage:
- Proactive creation:
SportingEventCreatedEventcauses the integrator to execute aSetScoreBoardDetailsCommandvia the scopedICommandExecutor. - Reactive backfill:
ScoreBoardDataRequestedEventloads theSportingEventContractfrom the document store and backfills details; when the sporting event is missing, no command is executed. - Logging: information is logged for both proactive creation and backfill.
Projection builder (ScoreBoardContractBuilderTests.cs)¶
ScoreBoardContractBuilder has no dependencies, so these tests call Apply directly. Coverage includes applying ScoreBoardDetailsSetEvent to produce a typed CricketScoreBoardContract, applying CricketScoreSetEvent / CricketTossOutcomeSetEvent / MatchSummarySetEvent, and the Rugby League equivalents, with Shouldly assertions on the resulting read model.
Test infrastructure¶
- Aggregate tests build a real
CommandExecutorover anInMemoryDomainEventStore, with commands/events registered throughDomainTypeMappingsand aTestInstanceResolver. Tests assert on the events returned fromExecuteAsyncand pass the expected aggregate version explicitly (note: the self-healingScoreBoardDataRequestedEventadds to the version count on a new aggregate). - Integrator tests use
NSubstitute(Substitute.For<ICommandExecutor>(),Substitute.For<IDocumentStore>(),Substitute.For<ILogger<ScoreBoardIntegrator>>()) and verify command execution / logging. - Projection tests call the static
ScoreBoardContractBuilder.Apply(...)and assert withShouldly.
Running the tests¶
These are xUnit v3 tests, so run them with dotnet run against the test project (not dotnet test). xUnit v3 takes filter flags after a -- separator.
# Run the whole unit-test project (includes all ScoreBoard tests)
dotnet run --project src/Tests/LBS.UnitTests/LBS.UnitTests.csproj
# Run only the ScoreBoard aggregate tests (all four partial files share this class)
dotnet run --project src/Tests/LBS.UnitTests/LBS.UnitTests.csproj -- \
--filter-class "LBS.UnitTests.Scoreboard.ScoreBoardAggregateTests"
# Run the integrator or projection-builder tests
dotnet run --project src/Tests/LBS.UnitTests/LBS.UnitTests.csproj -- \
--filter-class "LBS.UnitTests.Scoreboard.ScoreBoardIntegratorTests"
dotnet run --project src/Tests/LBS.UnitTests/LBS.UnitTests.csproj -- \
--filter-class "LBS.UnitTests.Scoreboard.ScoreBoardContractBuilderTests"
# Run a single test by method name
dotnet run --project src/Tests/LBS.UnitTests/LBS.UnitTests.csproj -- \
--filter-method "*Execute_SetCricketScoreCommand_OnNewAggregate_ShouldRaiseDataRequestedAndCricketScoreSetEvents"
Design patterns exercised¶
- Event sourcing - all state changes flow through events applied back onto the aggregate.
- Self-healing - sport commands that arrive before details emit
ScoreBoardDataRequestedEventso an integrator can backfill. - Idempotency - every
Executereturns early when the command carries no change. - CQRS - commands mutate the aggregate;
ScoreBoardContractBuilderprojects events into the read model. - Event-driven integration -
ScoreBoardIntegratorreacts to sporting-event and data-requested events. - Domain validation - business rules (sport match, team composition, ball numbering, clock state) are enforced in
Validate. - Partial-class organisation - both the aggregate and its aggregate test class are split per sport.