DEV Community

Alex Aslam
Alex Aslam

Posted on

Testing Event-Sourced Systems: No More Fixtures, Just Replays

"Your test suite is a time machine—if you built it right."

Traditional testing in Rails?

  • Fixtures: Stale, brittle snapshots of fake data.
  • Mocks: Lie about how the system behaves.
  • Factories: Slow, complex setups.

Event sourcing flips the script. Instead of faking state, replay real history.

Here’s how to test without losing your sanity.


1. The Power of Event-Based Testing

Traditional Testing (CRUD)

# Setup
user = create(:user, balance: 100)

# Test
post :withdraw, params: { amount: 50 }
expect(user.reload.balance).to eq(50)
Enter fullscreen mode Exit fullscreen mode

Problems:

  • Requires database.
  • Tests state, not behavior.

Event-Sourced Testing

# Setup
events = [
  Events::UserRegistered.new(user_id: "u1"),
  Events::BalanceDeposited.new(user_id: "u1", amount: 100)
]

# Test
command = Commands::Withdraw.new(user_id: "u1", amount: 50)
new_events = command.execute

expect(new_events).to include(
  an_instance_of(Events::BalanceWithdrawn)
    .with(amount: 50)
)
Enter fullscreen mode Exit fullscreen mode

Wins:

  • No database needed.
  • Tests business rules, not persistence.

2. Testing Strategies

Unit Tests: Commands & Aggregates

Test decisions, not side effects:

it "rejects overdrafts" do
  events = [Events::BalanceDeposited.new(amount: 100)]
  account = Account.new(events)

  expect {
    account.withdraw(200)
  }.to raise_error(Account::InsufficientFunds)
end
Enter fullscreen mode Exit fullscreen mode

Integration Tests: Event Handlers

Verify projections and side effects:

it "upgrades user on 3rd deposit" do
  events = 2.times.map { Events::Deposited.new }
  handler = Handlers::Loyalty.new

  new_events = handler.process(events)
  expect(new_events).to include(Events::UserUpgraded)
end
Enter fullscreen mode Exit fullscreen mode

End-to-End: Replay Real Scenarios

Test entire workflows from production data (sanitized):

it "replays checkout flow" do
  events = EventStore.load(stream_id: "prod_order_123")
  projection = OrderProjection.new(events)

  expect(projection.status).to eq(:fulfilled)
end
Enter fullscreen mode Exit fullscreen mode

3. Tools to Make It Easier


4. When to Avoid This Approach

Simple CRUD: Overkill for basic forms.
Legacy systems: Requires event-sourcing adoption first.


"But We Have 1000 Tests Relying on Fixtures!"

Start small:

  1. Add event publishing to one critical feature.
  2. Keep old tests, but write new ones against events.
  3. Gradually migrate as you refactor.

Have you tried event-based testing? Share your wins (or horror stories) below.

Top comments (0)