Real-World Event Replay Testing¶
This guide explains how to create tests that replay production/staging events through builders to verify correct behavior and debug issues.
Overview¶
Real-world testing allows you to:
- Replay actual events from production/staging through the ScoreCardContractBuilder
- Verify the builder produces correct output for real match data
- Debug issues by stopping at specific event versions
- Create regression tests using actual event sequences
Quick Start¶
public class MyMatchTests : ScoreCardEventReplayTestBase
{
protected override string EventsJsonFileName => "Scoreboard/RealWorld/my_events.json";
protected override int? MaxEventVersion => null; // null = all events
public MyMatchTests()
{
this.Initialize(); // Must call in constructor
}
[Fact]
public void ShouldHaveCorrectScore()
{
this.Result.Innings[0].TotalRuns.ShouldBe(150);
}
}
Step-by-Step Guide¶
Step 1: Export Events from Event Store¶
Export the events for a specific aggregate (ScoreBoard) as JSON. You can use the Event History panel in the UI or query the database directly.
The JSON format should be:
[
{
"eventId": "019ab935-5f0f-4917-993e-e84c670c3c62",
"aggregateRootId": "53f7ce4f-d801-55a2-b126-7eeae3802690",
"eventType": "CricketBallByBallDetailsSetEvent",
"version": 1,
"timestamp": "2025-11-25T04:11:14.077231+00:00",
"userId": "00000000-0000-0000-0000-000000000000",
"userName": "System",
"eventData": {
"sportingEventId": "f2664971-1da0-56f9-bee7-69a5d9a155ec",
// ... event-specific data
}
}
]
Step 2: Add JSON File to Test Project¶
- Copy the JSON file to:
src/Tests/LBS.UnitTests/Scoreboard/RealWorld/ - Name it descriptively:
{aggregateId}_events.json(e.g.,53f7ce4f-d801-55a2-b126-7eeae3802690_events.json)- Or use match description (e.g.,
adelaide-strikers-vs-brisbane-heat_events.json) - The
.csprojalready includes JSON files automatically:
Step 3: Create Test Class¶
Create a new test class inheriting from ScoreCardEventReplayTestBase:
using LBS.Domain.Sport;
using LBS.Domain.Sport.Participant;
using Shouldly;
namespace LBS.UnitTests.Scoreboard.RealWorld;
public class MyMatchTests : ScoreCardEventReplayTestBase
{
protected override string EventsJsonFileName => "Scoreboard/RealWorld/my_events.json";
/// <summary>
/// Process all events (null = no limit).
/// </summary>
protected override int? MaxEventVersion => null;
public MyMatchTests()
{
this.Initialize(); // Must call in constructor
}
[Fact]
public void ShouldHaveCricketSport()
{
this.Result.Sport.ShouldBe(Sports.Cricket);
}
[Fact]
public void ShouldHaveCorrectTotalRuns()
{
var innings1 = this.Result.Innings.First(i => i.InningsNumber == 1);
innings1.TotalRuns.ShouldBe(150);
}
[Fact]
public void SpecificPlayer_ShouldHaveCorrectStats()
{
var playerId = new ParticipantId(new Guid("75b6d0ae-940c-5ce2-b4ab-56645257e440"));
var batter = this.Result.Innings
.SelectMany(i => i.Batting)
.FirstOrDefault(b => b.BatterId == playerId);
batter.ShouldNotBeNull();
batter.Runs.ShouldBe(11);
batter.BallsFaced.ShouldBe(14);
}
}
Step 4: Test at Specific Event Versions¶
To debug issues, create multiple test classes with different MaxEventVersion values:
/// <summary>
/// Tests state after setup events only (lineup, toss, etc.)
/// </summary>
public class MyMatch_AfterSetup : ScoreCardEventReplayTestBase
{
protected override string EventsJsonFileName => "Scoreboard/RealWorld/my_events.json";
protected override int? MaxEventVersion => 5;
public MyMatch_AfterSetup()
{
this.Initialize();
}
[Fact]
public void ShouldHaveTeamsButNoScore()
{
this.Result.Teams.TeamOne.ShouldNotBeNull();
this.Result.Innings.FirstOrDefault()?.TotalRuns.ShouldBe(0);
}
}
/// <summary>
/// Tests state after first ball-by-ball events
/// </summary>
public class MyMatch_AfterFirstOver : ScoreCardEventReplayTestBase
{
protected override string EventsJsonFileName => "Scoreboard/RealWorld/my_events.json";
protected override int? MaxEventVersion => 15;
public MyMatch_AfterFirstOver()
{
this.Initialize();
}
[Fact]
public void ShouldHaveSomeRuns()
{
this.Result.Innings[0].TotalRuns.ShouldBeGreaterThan(0);
}
}
Dynamic Version Testing¶
You can also use ReplayEventsUpTo() within a single test for dynamic version testing:
[Fact]
public void ShouldProgressCorrectlyThroughVersions()
{
var atVersion5 = this.ReplayEventsUpTo(5);
atVersion5.Innings.Count.ShouldBe(0); // No innings yet
var atVersion20 = this.ReplayEventsUpTo(20);
atVersion20.Innings.Count.ShouldBe(1); // First innings started
var atVersion100 = this.ReplayEventsUpTo(100);
atVersion100.Innings[0].TotalRuns.ShouldBeGreaterThan(50);
}
Available Properties¶
| Property | Type | Description |
|---|---|---|
this.Result |
CricketScoreCardContract |
The scorecard after replaying events |
this.AllEvents |
List<AggregateEventRecord> |
All loaded events for inspection |
Supported Event Types¶
Events processed by the builder:
- CricketBallByBallDetailsSetEvent - Ball-by-ball match data
- CricketLineupSetEvent - Team lineups
- CricketTossOutcomeSetEvent - Toss results
Events skipped (not handled by ScoreCardContractBuilder):
- ScoreBoardDataRequestedEvent
- ScoreBoardDetailsSetEvent
- CricketScoreSetEvent
Running Tests¶
# These are xUnit v3 tests — run them with `dotnet run` (not `dotnet test`),
# passing filter flags after a `--` separator.
# Run all real-world tests
dotnet run --project src/Tests/LBS.UnitTests/LBS.UnitTests.csproj -- \
--filter-class "*RealWorld*"
# Run specific match tests
dotnet run --project src/Tests/LBS.UnitTests/LBS.UnitTests.csproj -- \
--filter-class "*MyMatchTests*"
# Run a specific test
dotnet run --project src/Tests/LBS.UnitTests/LBS.UnitTests.csproj -- \
--filter-method "*SpecificPlayer_ShouldHaveCorrectStats*"
Debugging Tips¶
-
Find the problematic event version: Use binary search with
MaxEventVersionto narrow down which event causes issues -
Inspect intermediate state: Add debug assertions or use
ReplayEventsUpTo()to check state at different points -
Check event data: Use
this.AllEventsto inspect the raw event data: -
Compare with UI: Use the Event History panel in the UI to see the same events and compare expected vs actual state
SuperCoach Stats Validation¶
In addition to scorecard validation, you can validate that SuperCoach fantasy stats are calculated correctly from the scorecard. This provides end-to-end validation of:
- Ball-by-ball events → Scorecard processing
- Scorecard → SuperCoach stats calculation
- SuperCoach stats → Fantasy points derivation
Creating a SuperCoach Stats Validation Test¶
using LBS.Domain.Fantasy.SuperCoachPlayerStats.Extensions;
using LBS.Domain.Fantasy.SuperCoachPlayerStats.Queries;
using LBS.Domain.Sport.Participant;
using Shouldly;
namespace LBS.UnitTests.Scoreboard.RealWorld;
public class MyMatch_SuperCoachStatsValidation : ScoreCardEventReplayTestBase
{
protected override string EventsJsonFileName => "Scoreboard/RealWorld/my_events.json";
protected override int? MaxEventVersion => null;
private BblSupercoachMatchStatsQueryResponseContract SuperCoachStats { get; set; } = null!;
public MyMatch_SuperCoachStatsValidation()
{
this.Initialize();
// Calculate SuperCoach stats from the scorecard
this.SuperCoachStats = this.Result.CalculateCricketSuperCoachStats();
}
[Fact]
public void PlayerStats_ShouldNotBeEmpty()
{
this.SuperCoachStats.PlayerStats.ShouldNotBeEmpty();
}
[Fact]
public void SpecificPlayer_ShouldHaveCorrectBattingStats()
{
var playerId = new ParticipantId(new Guid("75b6d0ae-940c-5ce2-b4ab-56645257e440"));
var stats = this.SuperCoachStats.PlayerStats.GetValueOrDefault(playerId);
stats.ShouldNotBeNull();
stats.Runs.ShouldBe(13);
stats.BallsFaced.ShouldBe(17);
stats.Fours.ShouldBe(1);
stats.Sixes.ShouldBe(0);
}
[Fact]
public void SpecificPlayer_ShouldHaveCorrectBowlingStats()
{
var playerId = new ParticipantId(new Guid("0c1a8103-070e-537c-a9ff-876d9b1cdaee"));
var stats = this.SuperCoachStats.PlayerStats.GetValueOrDefault(playerId);
stats.ShouldNotBeNull();
stats.OversBowled.ShouldBe(2);
stats.RunsConceded.ShouldBe(10);
stats.Wickets.ShouldBe(0);
stats.DotBalls.ShouldBe(5);
}
[Fact]
public void SpecificPlayer_ShouldHaveCorrectFantasyPoints()
{
var playerId = new ParticipantId(new Guid("75b6d0ae-940c-5ce2-b4ab-56645257e440"));
var stats = this.SuperCoachStats.PlayerStats.GetValueOrDefault(playerId);
stats.ShouldNotBeNull();
stats.BattingPoints.ShouldBe(13); // 13 runs
stats.BowlingPoints.ShouldBeGreaterThanOrEqualTo(0);
stats.FieldingPoints.ShouldBeGreaterThanOrEqualTo(0);
}
}
Available SuperCoach Stats¶
| Category | Stats |
|---|---|
| Batting | Runs, BallsFaced, Fours, Sixes, StrikeRate |
| Bowling | OversBowled, BallsBowled, RunsConceded, Wickets, MaidenOvers, DotBalls, Wides, NoBalls, EconomyRate |
| Fielding | Catches, RunOuts, Stumpings |
| Fantasy Points | BattingPoints, BowlingPoints, FieldingPoints, LivePoints |
| Bonus Points | StrikeRateBonusPoints, RunsBonusPoints, WicketsBonusPoints, EconomyRateBonusPoints |
Fantasy Points Formulas¶
| Component | Formula |
|---|---|
| Runs Scored | runs × 1 |
| Strike Rate Bonus | SR≥160→+25, 150-159→+20, 140-149→+15, 130-139→+10, 120-129→+5 (requires ≥20 runs) |
| Runs Bonus | 100+→+20, 50-99→+10 |
| Wickets Taken | wickets × 20 |
| Wickets Bonus | floor(wickets/3) × 10 |
| Maiden Overs | maidens × 15 |
| Dot Balls | dots × 1 |
| Extras Conceded | (wides + no-balls) × -1 |
| Economy Rate Bonus | ER≤4→+25, ≤5→+20, ≤6→+15, ≤7→+10, ≤8→+5 (requires ≥3 overs) |
| Catches | catches × 10 |
| Run Outs | run-outs × 20 |
| Stumpings | stumpings × 15 |
Helper Methods Pattern¶
For comprehensive tests, use helper methods to validate each stat category:
private void AssertBattingStats(ParticipantId playerId, string playerName,
int expectedRuns, int expectedBallsFaced, int expectedFours, int expectedSixes)
{
var stats = this.SuperCoachStats.PlayerStats.GetValueOrDefault(playerId);
stats.ShouldNotBeNull($"{playerName} should have stats");
stats.Runs.ShouldBe(expectedRuns, $"{playerName} runs mismatch");
stats.BallsFaced.ShouldBe(expectedBallsFaced, $"{playerName} balls faced mismatch");
stats.Fours.ShouldBe(expectedFours, $"{playerName} fours mismatch");
stats.Sixes.ShouldBe(expectedSixes, $"{playerName} sixes mismatch");
}
private void AssertBowlingStats(ParticipantId playerId, string playerName,
int expectedOvers, int expectedBalls, int expectedRunsConceded, int expectedWickets,
int expectedMaidens, int expectedDotBalls, int expectedWides, int expectedNoBalls)
{
var stats = this.SuperCoachStats.PlayerStats.GetValueOrDefault(playerId);
stats.ShouldNotBeNull($"{playerName} should have stats");
stats.OversBowled.ShouldBe(expectedOvers, $"{playerName} overs mismatch");
// ... other assertions
}
Running SuperCoach Stats Tests¶
# Run all SuperCoach stats validation tests
dotnet run --project src/Tests/LBS.UnitTests/LBS.UnitTests.csproj -- \
--filter-class "*SuperCoachStatsValidation*"
# Run fantasy points tests only
dotnet run --project src/Tests/LBS.UnitTests/LBS.UnitTests.csproj -- \
--filter-method "*FantasyPoints*"
File Locations¶
- Test base class:
src/Tests/LBS.UnitTests/Scoreboard/RealWorld/ScoreCardEventReplayTestBase.cs - Scorecard tests:
src/Tests/LBS.UnitTests/Scoreboard/RealWorld/AdelaideStrikersVsBrisbaneHeatWomenTests.cs - SuperCoach stats tests:
src/Tests/LBS.UnitTests/Scoreboard/RealWorld/AdelaideStrikersVsBrisbaneHeat_SuperCoachStatsValidation.cs - JSON files:
src/Tests/LBS.UnitTests/Scoreboard/RealWorld/*.json