Skip to content

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

  1. Copy the JSON file to: src/Tests/LBS.UnitTests/Scoreboard/RealWorld/
  2. Name it descriptively:
  3. {aggregateId}_events.json (e.g., 53f7ce4f-d801-55a2-b126-7eeae3802690_events.json)
  4. Or use match description (e.g., adelaide-strikers-vs-brisbane-heat_events.json)
  5. The .csproj already includes JSON files automatically:
    <Content Include="Scoreboard\RealWorld\*.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </Content>
    

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

  1. Find the problematic event version: Use binary search with MaxEventVersion to narrow down which event causes issues

  2. Inspect intermediate state: Add debug assertions or use ReplayEventsUpTo() to check state at different points

  3. Check event data: Use this.AllEvents to inspect the raw event data:

    var ballByBallEvents = this.AllEvents
        .Where(e => e.EventType == "CricketBallByBallDetailsSetEvent")
        .ToList();
    

  4. 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:

  1. Ball-by-ball events → Scorecard processing
  2. Scorecard → SuperCoach stats calculation
  3. 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