OpenCQRS 2.0: Tests That Read Like the Domain¶
In most software, testing is something you bolt on after the fact: you write the code, then you write tests to convince yourself the code does what you hoped. Event Sourcing quietly inverts that relationship. Because behavior is expressed as events, a test can read like a sentence about the domain: given these past events, when this command arrives, then these new events should follow. The test stops being scaffolding around the code and becomes the specification of what the system is supposed to do.
On June 26th, our friends at Digital Frontiers shipped OpenCQRS 2.0, and the change we keep coming back to leans all the way into that idea. The release modernizes plenty under the hood, but the part that matters most day to day is the redesigned testing support – a fluent DSL that makes those given/when/then specifications read less like test code and more like a conversation with the domain. Let's start with why that conversation is worth having at all.
Why Event Sourcing Tests Are Different¶
Think about how you test a typical state-based service. You set up a database, or you mock the repository that talks to it. You arrange rows, call a method, then read the rows back and assert on them. Half the test is infrastructure, and the mocks encode your assumptions about collaborators rather than the behavior you actually care about. When such a test breaks, it is often the scaffolding that broke, not the logic.
Event Sourcing removes most of that ceremony. A command handler in this world is close to a pure function: you feed it the events that already happened and the command that just arrived, and it decides which new events should be appended. There is no hidden state, no clock you cannot control, and no network in the middle. The same inputs always produce the same decision.
That property is a gift for testing. A test can mirror the handler exactly: arrange the past as a list of events, act by sending a command, and assert on the events that come out. There is nothing to mock, because there are no collaborators standing between you and the decision. We have made this case before in Testing Without Mocks, and it holds especially well here, where the events are both the input and the output.
There is a second benefit that is easy to miss. A test written this way is documentation that cannot rot, because it runs. A new teammate reading "given a confirmed order, when a shipment is requested, then an OrderShipped event follows" learns the rule and gets proof that the rule still holds, in the same breath. The specification and the evidence that the code obeys it are the same artifact, which is something a state-based test full of mocks can rarely claim.
It also explains why a CQRS framework lives or dies by its testing story. If the whole point is that behavior is expressed as events, then the way you write tests is the way you talk about behavior. Frank Scheffler made exactly this argument when we sat down with him; if you want the background on how OpenCQRS approaches CQRS without forcing the aggregate pattern, our interview on OpenCQRS is a good place to start.
The Old Way Worked, but the Phases Were Implicit¶
OpenCQRS 1.0 already understood this. Its test fixture let you write a given/when/then test as a single chain of method calls, and for the simplest cases it read just fine:
This did the job. The friction was subtler than any missing feature: every assertion began with the same expect prefix, and that repetition was the visible symptom of a structural issue underneath. Setup, execution, and assertion all hung off one flat surface, so the given/when/then phases the test was meant to express were never actually separated in the code. They were a convention you kept in mind, not a structure the API enforced.
That is the problem the redesign went after. A test that needs to say "given a book that is already lent, when someone tries to borrow it, then the command fails with a specific exception" should be assembled from three distinct steps that mirror that sentence – arrange, act, assert – rather than a single chain in which the boundaries between them are left for the reader to infer.
A DSL That Reads Like the Domain¶
OpenCQRS 2.0 replaces the old fixture API with a fluent Given/When/Then DSL, and the difference is immediately visible. The same simple test now reads as three clearly separated phases:
fixture.given()
.nothing()
.when(command)
.succeeds()
.allEvents()
.single(event -> event.comparing(expectedEvent));
Read it top to bottom and it narrates itself: given nothing, when this command, then it succeeds and produces exactly one event equal to the one you expected. The phases are no longer a flat chain – given() opens the arrangement, when() performs the act, and the assertions that follow describe the outcome.
Getting a fixture is just as low-ceremony. You annotate a test class with @CommandHandlingTest and let Spring inject a CommandHandlingTestFixture for the command you want to exercise. The slice wires up only what command handling needs, so the test starts fast and stays focused on behavior, and the DSL takes over from there – no mock, no database, no broker in sight.
The arrangement is where the new DSL earns its keep. Instead of a single givenNothing(), you can describe a real history: choose the subject you are writing to, append events one after another, pin the time, and even advance the clock between them. That makes the awkward scenario from earlier read almost word for word like the domain sentence it describes:
fixture.given()
.usingSubject("/books/4711")
.event(e -> e.payload(new BookLentEvent("4711", "reader-1")))
.when(new BorrowBookCommand("4711", "reader-2"))
.fails()
.throwing(BookAlreadyLentException.class);
The outcome side is just as expressive as the arrangement side. Beyond succeeds() you can assert that a command fails(), and then say how: throwing(...) for a specific exception, or violatingExactly(...) when a consistency condition such as PRISTINE was the reason it was rejected. On the success path, you can drill into the produced events with allEvents().single(...), or assert on the rebuilt state with havingState(...) and stateSatisfying(...).
Time deserves a special mention. Plenty of domain rules are temporal – an offer expires, a grace period elapses, a rate limit resets – and testing them usually means fighting the system clock. The new arrangement lets you set an explicit starting instant with time(...) and advance it by a fixed timeDelta(...) between events, so a scenario like "given a reservation made an hour ago" can be stated directly rather than faked. Deterministic time turns flaky temporal tests into ordinary ones.
None of this changes what a test is for. It changes how close the test gets to the language you would use to describe the behavior out loud – which, in a system whose entire premise is expressing behavior as events, is exactly the point.
Tests Are Only as Good as Your Consistency Rules¶
A test is a promise, and a promise is only worth something if reality keeps it. A given/when/then test that asserts a command must fail when a book is already lent is only meaningful if the running system actually refuses that write. This is where the rest of OpenCQRS 2.0 quietly supports the testing story.
OpenCQRS leans on EventSourcingDB's preconditions to enforce consistency at the moment of writing, and 2.0 makes that vocabulary richer. There are two new preconditions: SubjectIsPopulated, which requires that a subject already has events, and EventQlQueryIsTrue, which lets a write depend on the result of an EventQL query. Together they let you express conditions that used to live scattered across application code as guarantees the database itself checks.
Consider a rule like "a member may hold at most three loans at once." Before, you would source the member's events, count the active loans in application code, and hope no concurrent command slipped a fourth one in between your check and your write. With EventQlQueryIsTrue, the limit becomes part of the write itself: the new events are appended only if the query still holds at commit time. The race condition you used to reason about nervously simply cannot happen – and the test that asserts the limit is exercising the very same guarantee.
The framework now maps a command's intent onto those preconditions more precisely. A handler that declares its subject must already exist is backed by SubjectIsPopulated at the final write; one that requires a pristine subject is backed by SubjectIsPristine; and the sourcing mode decides exactly which subjects get optimistic-concurrency checks. Command handlers can also attach their own preconditions when they publish, including for command-relative subjects, so a single command can assert a fact about a different part of the stream hierarchy before it commits.
The payoff for testing is subtle but important. The rules you assert in a given/when/then test are the same rules EventSourcingDB enforces at the write – not a parallel reimplementation that can drift out of sync. We have argued that consistency is a business decision rather than a technical afterthought, and preconditions are how that decision becomes executable. If you want the mechanics, the preconditions documentation walks through each one.
When the Test Becomes the Spec¶
We opened with the claim that in Event Sourcing, the test is the specification rather than an afterthought. OpenCQRS 2.0 is what it looks like when a framework takes that claim literally – a testing DSL that reads like a description of behavior, backed by consistency rules the database enforces so those descriptions stay true. The platform work underneath keeps it current; the testing work on top is what makes it a pleasure to use.
It is also another sign of the ecosystem we always hoped EventSourcingDB would grow. When we celebrated OpenCQRS 1.0, the milestone was that the framework existed at all. With 2.0, the story is maturity – the same vision, refined by the teams running it in production.
What Else Shipped¶
Two more additions are worth calling out. The client gained readSubjects, which lists the subjects beneath a base subject – a small API with outsized value for hierarchical streams, where discovering what exists under /books or /orders is suddenly a single call. Think of a reporting job that needs to walk every order placed today, or an admin screen that lists the active loans for a member without you keeping a separate index: instead of guessing identifiers or maintaining a side lookup, you ask the store what is there. It is the kind of thing that makes diagnostics and administrative tooling far easier to build, and it leans directly on one of EventSourcingDB's quieter strengths: streams that nest.
The other theme is modernization. OpenCQRS 2.0 moves onto a current foundation – Spring Boot 4.1, Jackson 3, Gradle 9, and EventSourcingDB 1.2, with the event-processor autoconfiguration adopting Spring's new bean registration API and the codebase picking up JSpecify nullness annotations. If you want to know what arrived in the database release it now builds on, our EventSourcingDB 1.2.0 announcement has the details.
That last item is quieter than the version bumps, but it earns a moment. JSpecify is an emerging, JVM-wide standard for expressing which references may be null and which may not, in a form that build tools, IDEs, and the Kotlin compiler all understand. The payoff shows up right at the boundary between your code and the framework: because OpenCQRS annotates its public API, your null-checker knows exactly where a null is allowed and where it is not, and can flag at compile time the mistakes that would otherwise surface as a NullPointerException at runtime – and for Kotlin callers, that same boundary stops leaking unchecked platform types.
A major version means a few sharp edges on the way up. The biggest one is, fittingly, the tests: the old fixture API is gone, so existing tests move to the new DSL. The Jackson 3 upgrade also shifts serializer packages from com.fasterxml.jackson to tools.jackson, which matters if you wrote custom marshallers. There is no separate migration guide to point you at, but it is worth saying plainly: the rewrite that costs you the most effort is the one that leaves your tests reading better than they did before.
If you build on the JVM and want your tests to read like the domain they describe, this is a good moment to look. OpenCQRS 2.0 is available now, and if you are starting from scratch, the fastest path is to bring up EventSourcingDB first: installing EventSourcingDB takes about a minute, and from there the given/when/then loop is yours to try. We'd love to hear how it reads in your own domain – tell us at hello@thenativeweb.io.