Skip to content

Document unit testing event-sourced handlers with StubEventStream (#5455, #5458) - #5463

Merged
jeremydmiller merged 1 commit into
masterfrom
gh-5455
Sep 21, 2026
Merged

jeremydmiller merged 1 commit into
masterfrom
gh-5455

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Closes #5455
Closes #5458

The two issues are the same ask from different angles, so they land together. StubEventStream<T> has lived in src/Marten/Events/ for years and grep -r StubEventStream docs/ returned nothing — its only published exposure was as an aside on a Wolverine page. docs/testing/ had exactly one page, integration.md; this is the other half of that section.

Part 1 — the docs (the actual ask)

New docs/testing/unit-testing-event-handlers.md, added to the Testing nav beside Integration Testing. Every sample is pulled from src/EventSourcingTests/StubEventStreamTests.cs through mdsnippets, so it cannot drift from the API. It covers:

  • a single-stream handler taking IEventStream<T> — construct the stub, call it, assert on EventsAppended
  • a null aggregate as a stream that does not exist yet, which is how a handler chooses between starting and appending
  • the "nothing to append" outcome, as its own test
  • a two-stream command, with Id/Key wiring the command's identities to the streams
  • EventsAppended (raw bodies) vs Events (IEvent envelopes)
  • StartingVersion / CurrentVersion for a version-sensitive handler
  • why this beats Substitute.For<IEventStream<T>>() + Received(1).AppendOne(...) — that verifies a call, not an outcome, and breaks on a behavior-preserving refactor
  • ⚠️ the boundary: the stub records. It does not persist, project, validate, or ever throw ConcurrencyException. Those belong in integration.md.

Cross-referenced from command_handler_workflow.md (per #5455) and from integration.md.

StubEventStreamTests.cs was two assertions long and is now the snippet source, with the original append_one/append_many kept so Marten's subclass stays covered.

Part 2 — Marten's own copy

Taking option (1) from #5458: Marten.Events.StubEventStream<T> is now a thin subclass of JasperFx.Events.StubEventStream<T>. Existing tests keep compiling, there is one implementation rather than two types of the same name behaving slightly differently, and the docs name the JasperFx type as the one to reach for. As the issue predicted, this widens Key to string? and makes Cancellation settable.

⚠️ This carries a JasperFx bump: 2.72.0 → 2.73.2

The store-agnostic type shipped in 2.73.0, so the docs cannot be written against it without the bump. Directory.Packages.props gets a note in the file's usual style. Do not pin 2.73.0 — #859/#863 dropped EventModelClaim's two-argument constructor there and restored it in 2.73.1. Nothing in Marten binds that type today, but the floor is 2.73.1 for anyone who does.

Everything else in the span is inert here: an event-model source disagreement naming its source, ModelCollapse carrying structured names, and a codegen test fix.

A bug found while writing the alias sample

The StoreOptions overload never worked. It built new EventGraph(options) — a second graph with its own empty event type cache — so an alias registered through options.Events.MapEventType<T>("...") never reached the stream, and Events reported the conventional name anyway. That is the one thing the overload exists for; its doc comment says "you only care about this overload if you are customizing event type aliases".

It now uses options.EventGraph, the graph the aliases were actually configured on, and marten_stub_honors_configured_event_aliases pins it. (This test failed against the old behavior, which is how it surfaced.)

Verification

All on PostgreSQL 17:

  • EventSourcingTests 2280/2280
  • CoreTests 674/674
  • DaemonTests 323/323
  • DaemonTests.ManualOnly compiles (no CI job builds it)
  • full src/Marten.slnx build clean
  • vitepress build docs clean, all 7 snippets resolved

mdsnippets rewrites ~30 doc files with pre-existing drift on master; those were reverted, so the only doc changes here are this page, the two cross-reference tips, and the nav entry.

Not in scope

The Wolverine-side cross-reference is JasperFx/wolverine#4470, and the sibling Polecat and Fisher doc pages are tracked in their own repos.

🤖 Generated with Claude Code

https://claude.ai/code/session_01481eiEZg1Bv6pu4DrgPRg3

, #5458)

Closes #5455 and #5458, which are the same ask from two angles: StubEventStream<T>
has lived in src/Marten/Events/ for years and `grep -r StubEventStream docs/`
returned nothing. docs/testing/ had exactly one page, integration.md, and the gap
was the other half -- unit testing an event-sourced decision without Postgres.

Part 1, the docs. New docs/testing/unit-testing-event-handlers.md, beside
Integration Testing in the Testing nav, with every sample pulled from
src/EventSourcingTests/StubEventStreamTests.cs through mdsnippets so it cannot
drift from the API. It covers the single-stream handler, a null aggregate as a
stream that does not exist yet, the "nothing to append" outcome, a two-stream
command with Id/Key wiring, EventsAppended (raw bodies) vs Events (IEvent
envelopes), the settable versions and identities, why this beats mocking
IEventStream<T>, and the boundary -- the stub records, it does not persist,
project, or ever throw ConcurrencyException. Cross-referenced from
command_handler_workflow.md and integration.md.

StubEventStreamTests.cs was two assertions long; it is now the snippet source,
with the original append_one/append_many kept so Marten's subclass stays covered.

Part 2, what happens to Marten's copy. Taking option (1) from #5458:
Marten.Events.StubEventStream<T> is now a thin subclass of
JasperFx.Events.StubEventStream<T>, so existing tests keep compiling and there is
one implementation. As the issue predicted, this widens Key to string? and makes
Cancellation settable. The docs name the JasperFx type as the one to reach for.

This needs JasperFx 2.72.0 -> 2.73.2, since the store-agnostic type shipped in
2.73.0. See the new note in Directory.Packages.props -- and do not pin 2.73.0,
which dropped EventModelClaim's two-argument constructor before 2.73.1 restored it.

Found while writing the alias sample: the StoreOptions overload never worked. It
built `new EventGraph(options)`, a second graph with its own empty event type
cache, so an alias registered through options.Events.MapEventType<T>("...") never
reached the stream and Events reported the conventional name anyway -- the one
thing that overload exists for. It now uses options.EventGraph, and the new
marten_stub_honors_configured_event_aliases test pins it.

Verified on PostgreSQL 17: EventSourcingTests 2280/2280, CoreTests 674/674,
DaemonTests 323/323, DaemonTests.ManualOnly compiles, full solution builds, and
the vitepress docs build is clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01481eiEZg1Bv6pu4DrgPRg3
@jeremydmiller
jeremydmiller merged commit 5b9b7a3 into master Sep 21, 2026
32 checks passed
@jeremydmiller
jeremydmiller deleted the gh-5455 branch September 21, 2026 12:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant