Testing the Domain
In this tutorial you will learn, how to write unit test for your command handlers. The tests verify the domain logic by focussing on:
- sending commands to command handlers for execution
- implicitly verifying that the write mode is properly rebuilt
- verifying the publication of new events
Preparing the Test Fixture
In order to test the command handling logic, a new JUnit test class needs to be created
in src/test/java/com/example/cqrs
, as follows:
The test contains the following essential elements:
- It is annotated with
@CommandHandlingTest
, which declares the test class as a Spring Boot Test, designated for testing@CommandHandling
annotated methods (line 17). - It defines a Mockito bean for the
LateChargeCalculator
, since this dependency is out of scope of this test class and hence will be mocked (lines 20-21). -
It defines an initial test method with an autowired
CommandHandlingTestFixture
. This fixture substitutes the command execution via theCommandRouter
. The command handler to test is identified by the fixture's generic types in the following order (lines 23-26):- the type of the state rebuilt prior to executing the command (
Void
) - the type of the command executed (
PurchaseBookCommand
) - the command handler's return type (
Void
)
- the type of the state rebuilt prior to executing the command (
For the remainder of this tutorial we will be implementing further tests, by simply adding them to the BookHandlingTest
class.
All tests can be executed directly from the IDE or as follows:
Purchasing Books
The first test is going to verify that a new book copy can be purchased successfully, i.e. that
a valid PurchaseBookCommand
is handled successfully and results in a new BookPurchasedEvent
being published.
The CommandHandlingTestFixture
lets
us express this using its Given When Then fluent API, as follows:
@Test
public void canBePurchased(@Autowired CommandHandlingTestFixture<Void, PurchaseBookCommand, Void> fixture) {
var id = UUID.randomUUID();
fixture
// given
.givenNothing()
// when
.when(
new PurchaseBookCommand(
id,
"978-0008471286",
"Lord of the Rings",
"JRR Tolkien",
1248L
)
)
// then
.expectSuccessfulExecution()
.expectSingleEvent(
new BookPurchasedEvent(
id,
"978-0008471286",
"Lord of the Rings",
"JRR Tolkien",
1248L
)
);
}
Borrowing Books
Secondly, we will test that a book copy can be borrowed. For this, the previously expected BookPurchasedEvent
is
given to the test fixture, before the command execution. Upon successful execution a BookLentEvent
is expected
to be published, as well as the due date being returned from the command handler. The test can be expressed as follows:
Tip
Since the due date is randomly generated by the command handler, it cannot be verified by equality. Instead, AssertJ is used here to verify both the return value from the command handler (line 17) and the due date contained within the published event (line 20).
Furthermore, it should be guaranteed that book copies can no longer be borrowed, if currently lent. This can be verified by another test for the same command handler, which expects no events but an exception instead, as follows:
@Test
public void cannotBeBorrowedIfAlreadyLent(@Autowired CommandHandlingTestFixture<Book, BorrowBookCommand, Instant> fixture) {
var id = UUID.randomUUID();
fixture
.given(
new BookPurchasedEvent(
id,
"978-0008471286",
"Lord of the Rings",
"JRR Tolkien",
1248L
)
)
.andGiven(
new BookLentEvent(
id,
Instant.now().plus(3, ChronoUnit.DAYS)
)
)
.when(new BorrowBookCommand(id))
.expectException(IllegalStateException.class)
.expectNoEvents();
}
Returning Books
Upon returning books to the library, we need to distinguish, if the return is overdue or not, i.e. if a late
charge is due or not. The LateChargeCalculator
is responsible for calculating the fee, while the command handler
decides, if the return is overdue or not. So, for returns without late charge the test may look as follows:
@Test
public void canBeReturnedWithoutLateCharge(@Autowired CommandHandlingTestFixture<Book, ReturnBookCommand, Void> fixture) {
var id = UUID.randomUUID();
var now = Instant.now();
fixture
.given(
new BookPurchasedEvent(
id,
"978-0008471286",
"Lord of the Rings",
"JRR Tolkien",
1248L
)
)
.andGiven(
new BookLentEvent(
id,
now.plus(1, ChronoUnit.DAYS)
)
)
.when(new ReturnBookCommand(id))
.expectSuccessfulExecution()
.expectSingleEvent(new BookReturnedEvent(id, 0.0));
}
For overdue returns, the LateChargeCalculator
needs to be stubbed using Mockito to return a defined late charge for
the event publication, as follows:
@Test
public void canBeReturnedWithLateChargeCalculated(@Autowired CommandHandlingTestFixture<Book, ReturnBookCommand, Void> fixture) {
var id = UUID.randomUUID();
var now = Instant.now();
doReturn(3.2)
.when(lateChargeCalculator)
.calculateLateCharge(any());
fixture
.given(
new BookPurchasedEvent(
id,
"978-0008471286",
"Lord of the Rings",
"JRR Tolkien",
1248L
)
)
.andGiven(
new BookLentEvent(
id,
now.minus(1, ChronoUnit.DAYS)
)
)
.when(new ReturnBookCommand(id))
.expectSuccessfulExecution()
.expectSingleEvent(new BookReturnedEvent(id, 3.2));
}
You have now learned, how to write unit tests for the domain logic contained within command handlers, mocking any third-party dependencies, if necessary.