Skip to content

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 helper TryRaiseDataRequestedEvent. When a sport command arrives before the scoreboard details have been set (requiresAdditionalData == true and 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 from SetCricketScoreCommand when the incoming status is MatchStatuses.Completed and 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)

  • SetScoreBoardDetailsCommand tests - first-time details set raises ScoreBoardDetailsSetEvent; details arriving after a score has already created a partial scoreboard backfills metadata; null teams throw InvalidCommandException; identical details are idempotent; different teams raise a new event.
  • SetMatchSummaryCommand tests - valid summary raises MatchSummarySetEvent; same summary / different case / extra whitespace are all treated as no-change; a genuinely different summary raises a new event; empty SportingEventId, null/empty/whitespace summary all throw with specific messages; repeated updates apply the latest value.
  • SportingEventCompletedEvent tests - a SetCricketScoreCommand with MatchStatuses.Completed raises CricketScoreSetEvent followed by SportingEventCompletedEvent; the completed event fires only once across status transitions; event ordering (ScoreBoardDataRequestedEvent, then CricketScoreSetEvent, then SportingEventCompletedEvent) is asserted; a non-completed status does not raise it.

Cricket (ScoreBoardAggregateTests.Cricket.cs)

  • SetCricketScoreCommand tests - on a brand-new aggregate, raises ScoreBoardDataRequestedEvent plus CricketScoreSetEvent (self-healing); after details are set, raises only CricketScoreSetEvent; identical scores are idempotent; wrong sport, negative runs, and empty SportingEventId throw.
  • SetCricketTossOutcomeCommand tests - valid toss raises data-requested plus CricketTossOutcomeSetEvent with 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.)
  • CricketTossOutcomeSetEvent apply/state tests - confirm state is applied correctly on new and existing aggregates, across multiple sequential applies, and that the sport is set to Cricket.
  • UpdateCricketBallByBallDetailsCommand tests - adding balls to an in-progress match succeeds and raises CricketBallByBallDetailsSetEvent; plus a validation block covering empty SportingEventId, empty ball lists, and ball-numbering rules.

Rugby League (ScoreBoardAggregateTests.RugbyLeague.cs)

  • SetRugbyLeagueScoreCommand tests - first score, score/status/period updates, idempotency on identical scores, multi-period storage; on a new aggregate it raises ScoreBoardDataRequestedEvent plus RugbyLeagueScoreSetEvent; validation covers wrong sport, empty SportingEventId, negative points/tries, and empty periods.
  • SetRugbyLeagueParticipantStatsCommand tests - first stats, updates, idempotency, multiple participants tracked separately, full stat-field round-tripping, on-field toggling, and validation (wrong sport, empty SportingEventId, empty participant id, missing name); also verifies an uninitialised aggregate adopts Rugby League.

Rugby League match clock (ScoreBoardAggregateTests.RugbyLeagueMatchClock.cs)

  • SetRugbyLeagueMatchClockCommand tests - 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 plus RugbyLeagueMatchClockSetEvent; validation covers wrong sport, empty SportingEventId, 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: SportingEventCreatedEvent causes the integrator to execute a SetScoreBoardDetailsCommand via the scoped ICommandExecutor.
  • Reactive backfill: ScoreBoardDataRequestedEvent loads the SportingEventContract from 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 CommandExecutor over an InMemoryDomainEventStore, with commands/events registered through DomainTypeMappings and a TestInstanceResolver. Tests assert on the events returned from ExecuteAsync and pass the expected aggregate version explicitly (note: the self-healing ScoreBoardDataRequestedEvent adds 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 with Shouldly.

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

  1. Event sourcing - all state changes flow through events applied back onto the aggregate.
  2. Self-healing - sport commands that arrive before details emit ScoreBoardDataRequestedEvent so an integrator can backfill.
  3. Idempotency - every Execute returns early when the command carries no change.
  4. CQRS - commands mutate the aggregate; ScoreBoardContractBuilder projects events into the read model.
  5. Event-driven integration - ScoreBoardIntegrator reacts to sporting-event and data-requested events.
  6. Domain validation - business rules (sport match, team composition, ball numbering, clock state) are enforced in Validate.
  7. Partial-class organisation - both the aggregate and its aggregate test class are split per sport.