Skip to content

Command Handling Tests

OpenCQRS encapsulates the details of command handling by means of the CommandRouter. Hence, developers can focus on defining inputs in terms of commands, outputs in terms of newly published events, and the corresponding command handling logic.

While commands and events are typically designed as simple DTO classes, requiring no explicit tests. The command handling logic can (and should) be unit tested using the CommandHandlingTestFixture instead of using the CommandRouter for integration testing.

Command Handling Test Fixture

CommandHandlingTestFixture provides a given-when-then fluent API to write command handling tests. It mimics the behavior of the CommandRouter and therefore needs to be initialized with the CommandHandlerDefinition and its associated StateRebuildingHandlerDefinitions, prior to test execution.

Differences between CommandRouter and CommandHandlingTestFixture

CommandHandlingTestFixture mimics CommandRouter with the following exceptions:

  • No event store or EsdbClient is involved during test execution. All given and captured events are processed in-memory.
  • No event upcasting and type resolution is involved, that is the given events are passed as-is to the StateRebuildingHandlerDefinitions.
  • Raw Event information is stubbed, as no event store is involved, see the JavaDoc of CommandHandlerDefinition.
  • Automatic command meta-data propagation to newly published events is ignored.
  • Any state reconstructed prior to command execution won't be cached.

The following JUnit test exemplarily shows how to express tests using the CommandHandlingTestFixture for the PurchaseBookCommand handler:

@Test
public void bookCanBePurchased() {
    CommandHandlingTestFixture<PurchaseBookCommand> fixture = /* (1)! */

    fixture.given() /* (2)! */
            .nothing()
            .when(  /* (3)! */
                    new PurchaseBookCommand(
                            "4711",
                            "JRR Tolkien",
                            "LOTR",
                            435
                    )
            )
            .succeeds() /* (4)! */
            .allEvents()
            .exactly(  /* (5)! */
                    new BookPurchasedEvent(
                            "4711",
                            "JRR Tolkien",
                            "LOTR",
                            435
                    )
            );
}
  1. initialization omitted for brevity
  2. initializes the fixture with an empty state, i.e. no prior events published
  3. executes PurchaseBookCommand command handler, capturing any newly published events
  4. verifies successful command handler execution
  5. verifies that exactly one BookPurchasedEvent was published that equals the given reference event

Fluent API

CommandHandlingTestFixture requires a generic Command subtype identifying the command under test. Based on that, it provides a fluent API which can easily be inspected using IDE auto-completion in combination with the associated JavaDoc method descriptions.

Asserting Published Events

The fluent API offers two complementary entry points for event assertions on the success path:

  • .allEvents() operates on the complete captured event list without cursor state. Use it for global assertions.
  • .nextEvents() operates sequentially using a consuming cursor. Use it to navigate and assert in order.

Both interfaces share the same set of matcher methods with consistent semantics:

Matcher Asserts that...
single(consumer) exactly one event was captured/remains, and it matches the validation
once(consumer) the stream may have any length, but exactly one event matches the validation
any(consumer) at least one event matches the validation (stream must not be empty)
every(consumer) every event matches the validation (stream must not be empty)
none(consumer) no event matches the validation
exactly(payload, ...) the events match the given payloads in order, by Object.equals

single vs. once

single(...) is the right choice when the command handler is expected to publish exactly one event. It fail-fasts if the stream length is not 1, so a missing or duplicated event is reported immediately. once(...) is for streams where only one event is expected to match a particular predicate while other, unrelated events may be present.

exactly(payload, ...) compares payloads only

The exactly(...) shortcut compares each captured event's payload against the provided payload using Object.equals. Meta-data and subject are ignored. Use .matches(e -> e.asserting(...)) or single(e -> e.asserting(...)) to assert meta-data or subject in addition to the payload.

Manual Initialization

CommandHandlingTestFixture can be manually instantiated using the CommandHandlerDefinition under test and its associated StateRebuildingHandlerDefinitions, as shown in the following example:

@Test
public void bookCanBePurchased() {
    CommandHandlingTestFixture<PurchaseBookCommand> fixture =
            CommandHandlingTestFixture
                    .withStateRebuildingHandlerDefinitions(/* (1)! */)
                    .using(/* (2)! */);

    // ...
}
  1. Variable number of StateRebuildingHandlerDefinition instances. Could be omitted for this test, since PurchaseBookCommand is a creational command.
  2. The CommandHandlerDefinition to be tested.

Spring Boot Test Slice

When using Spring command handler definition and state rebuilding handler definition beans, test classes may be annotated using CommandHandlingTest, providing autoconfigured CommandHandlingTestFixture for every CommandHandlerDefinition within the Spring context. This relieves developer from manually initializing the fixture in favor of auto-wiring it, as shown in the following example:

@CommandHandlingTest
public class BookHandlingTest {

    @Test
    public void bookCanBePurchased(@Autowired CommandHandlingTestFixture<PurchaseBookCommand>/* (1)! */ fixture) {
        fixture.given()
                .nothing()
                .when(
                        new PurchaseBookCommand(
                                "4711",
                                "JRR Tolkien",
                                "LOTR",
                                435
                        )
                )
                .succeeds()
                .allEvents()
                .exactly(
                        new BookPurchasedEvent(
                                "4711",
                                "JRR Tolkien",
                                "LOTR",
                                435
                        )
                );
    }
}
  1. It is important to use the unambiguous generic command type, which uniquely identifies the autoconfigured fixture. Raw usage of CommandHandlingTestFixture isn't supported.

CommandHandlingTest is a so-called Spring Boot Test Slice, which executes the annotated test as Spring Boot test, but with reduced set of beans. The Spring beans (or annotated methods) included in the test context all must reside within classes annotated with CommandHandlerConfiguration.

Mocking

As CommandHandlingTest only recognizes Spring beans defined within CommandHandlerConfigurations supplementary bean dependencies required by any of the CommandHandlerDefinitions or StateRebuildingHandlerDefinitions under test, must be provided manually. As these represent dependent beans, this is preferably achieved using Spring's @MockitoBean annotation, which injects (or replaces) a mock bean into the context. The mock satisfies the bean dependency and may be stubbed and/or verified as part of the test.