Testing event sourcing applications with Axon
In my previous blog post about Axon, I showed how to create a simple application that uses event sourcing. In the end, you could call a REST API and test the application manually. I completely left out automated testing, even though I’m a big fan of Test-Driven Development. This was on purpose because I wanted to concentrate on the absolute basics. This time I wanted to show how to test applications with Axon.
As in the previous blog post, you can have a look at the complete project on GitHub: https://github.com/jd1/spring-boot-axon
JUnit5
I will write all the tests with JUnit 5. To support it you have to add JUnit 5 to the pom.xml and exclude the old JUnit:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>junit</groupId> <artifactId>junit</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-engine</artifactId> <scope>test</scope> </dependency> |
There are a few changes between JUnit4 and JUnit 5. If you want to know more about it, you can read the official user guide.
Testing the aggregate
The BankAccount from my previous post is a simple aggregate which contains methods to react to events. You can use these methods to apply some events and in the end, verify the state of the aggregate, in this case, the account balance:
1 2 3 4 5 6 7 8 |
@Test public void createAndDeposit() { BankAccount account = new BankAccount(); account.on(new AccountCreatedEvent("id", "Max", 0.0)); account.on(new MoneyDepositedEvent("id", 10.0)); assertEquals(10.0, account.getBalance()); } |
This test modifies the state of the aggregate without any additional framework (except Junit). It does not even call Axon specific code. This way of testing works as long as you don’t want to test commands. If you look into the implementation of e.g. BankAccount.on(DepositMoneyCommand), you will see that it calls
AggregateLifecycle.apply(new MoneyDepositedEvent(id, amount)); to publish an event. This will fail because Axon isn’t aware of the aggregate and cannot retrieve its lifecycle.
Axon Test Fixture
To test the command handling components Axon provides a test fixture. You can add it to your pom and start testing:
1 2 3 4 5 6 |
<dependency> <groupId>org.axonframework</groupId> <artifactId>axon-test</artifactId> <version>3.2</version> <scope>test</scope> </dependency> |
The test fixture provides an API where you define a test scenario with:
- the events that already happened
- the command you want to execute
- the events or exception you expect
Setup
Before you write the first test you need to instantiate the test fixture with the aggregate class that you want to test:
1 2 3 4 5 6 |
private FixtureConfiguration fixture; @BeforeEach public void setUp() { fixture = new AggregateTestFixture(BankAccount.class); } |
No prerequisites
A simple test without any events in the past can look like this:
1 2 3 4 5 6 7 |
@Test public void createAccount() { fixture.given() .when(new CreateAccountCommand("id", "Max")) .expectSuccessfulHandlerExecution() .expectEvents(new AccountCreatedEvent("id", "Max", 0.0)); } |
As you can see, the test has no prerequisites, it’s like working on an empty database. when(…) sends a command, in this case, to create a new bank account. Then you verify that everything is successful and the expected AccountCreatedEvent is emitted.
With previous events
If the test requires a certain state of an aggregate, i. e. the aggregate has already processed some command (which lead to events), you define one or more events as given. The state of the aggregate is then built upon these events.
1 2 3 4 5 6 7 |
@Test public void depositMoney() { fixture.given(new AccountCreatedEvent("id", "Max", 0.0)) .when(new DepositMoneyCommand("id", 12.0)) .expectSuccessfulHandlerExecution() .expectEvents(new MoneyDepositedEvent("id", 12.0)); } |
Exceptions
Of course, testing the happy path is not everything, you should also test commands that lead to exceptions. Instead of expectSuccessfulHandlerExecution() you call expectException(…):
1 2 3 4 5 6 |
@Test public void createExistingAccount() { fixture.given(new AccountCreatedEvent("id", "Max", 0.0)) .when(new CreateAccountCommand("id", "Max")) .expectException(EventStoreException.class); } |
In this case, the Axon Framework prevents an aggregate from being created twice. The same works if you try to send a command for an aggregate that does not exist. As a result, Axon will throw an AggregateNotFoundException:
1 2 3 4 5 6 |
@Test public void depositMoneyOnInexistentAccount() { fixture.given() .when(new DepositMoneyCommand("id", 12.0)) .expectException(AggregateNotFoundException.class); } |
But you can also test your own exceptions, e.g. if you try to get money from an account with an insufficient balance:
1 2 3 4 5 6 7 |
@Test public void overdrawAccount() { fixture.given(new AccountCreatedEvent("id", "Max", 0.0), new MoneyDepositedEvent("id", 10.0)) .when(new WithdrawMoneyCommand("id", 20.0)) .expectException(InsufficientBalanceException.class); } |
Conclusion
With little effort, and completely without Axon dependencies, it is possible to test the event processing of a single aggregate. If you want to go further and test commands, you can use the Axon Test Fixture. It provides a fluent API, which allows writing tests that are easy to read. You can set up an aggregate in the wanted state, apply new commands and verify the result. With this knowledge, you should be able to effectively test your event sourcing application and prevent bugs.
Recent posts






Comment article