Advanced Testing
After having taken note of the basic test flow and the use of matchers and field filters, it’s worth to investigate more advanced testing scenarios. This page covers those advanced tests, discussing integration tests, testing with spring boot, and testing with Testcontainers to name a few.
Integration testing
You are able to provide your entire application configuration since the AxonTestFixture expects the ApplicationConfigurer (as discussed here).
Furthermore, the broad given-execute and then-expect operations allow you to dive into configuration and use whatever components you require for setup and validation.
These two pointers combined make it straightforward to use Axon’s test fixtures for integration testing.
As such, examples of integration tests can roughly be consolidated to flows like this:
import org.axonframework.common.configuration.ApplicationConfigurer;
import org.axonframework.messaging.core.configuration.MessagingConfigurer;
import org.axonframework.modelling.repository.Repository;
import org.axonframework.test.fixture.AxonTestFixture;
import org.junit.jupiter.api.*;
import static org.assertj.core.api.Assertions.assertThat;
class AccountIntegrationTest {
private AxonTestFixture fixture;
@BeforeEach
void setUp() {
// One way or another, you need access to the full ApplicationConfigurer.
// In this example, the ApplicationConfigurer is constructed statically and accessed as such for tests.
fixture = AxonTestFixture.with(MainApp.configurer());
}
@Test
void test() {
fixture.given()
.events(new AccountCreatedEvent("account-1", 1337))
.execute(config -> {
// Retrieve components for setup
var repository = config.getComponent(Repository.class);
// Perform setup...
})
.when()
.command(new PlaceOrderCommand("order-1", "account-1"))
.then()
.expect(config -> {
// Retrieve components for verification
Repository repository = config.getComponent(Repository.class);
// Perform verification...
assertThat(repository).has(/*...*/);
});
}
@AfterEach
void tearDown() {
// Ensure to stop the fixture to cleanly close your resources
fixture.stop();
}
}
When using the fully declarative configuration route, you will be aware of your ApplicationConfigurer already and how to use it for fixtures.
If an autodetected flow is used, as with Spring, be sure to check out the testing with Spring Boot section.
Testing with Spring Boot
If you are using Spring Boot, you will not construct the ApplicationConfigurer yourself.
Axon Framework will take care of that for you.
However, that does not mean the ApplicationConfigurer does not exist in your application.
If you adjust your test case to be a Spring Boot test, you can inject the autoconfigured ApplicationConfigurer right in your fixture:
import org.axonframework.common.configuration.ApplicationConfigurer;
import org.axonframework.test.fixture.AxonTestFixture;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class AccountSpringTest {
@Autowired
private ApplicationConfigurer configurer;
private AxonTestFixture fixture;
@BeforeEach
void setUp() {
fixture = AxonTestFixture.with(configurer);
}
@Test
void testWithSpringConfiguration() {
fixture.given()
.event(new AccountCreatedEvent("account-1", 500.00))
.when()
.command(new WithdrawMoneyCommand("account-1", 100.00))
.then()
.success()
.events(new MoneyWithdrawnEvent("account-1", 100.00));
}
@AfterEach
void tearDown() {
fixture.stop();
}
}
From here, it is rather straightforward to add Testcontainers into the mix, when your (integration) test requires it. Be sure to check out the tests with Testcontainers section for that.
Verifying (mocked) Spring beans
As suggested in the integration testing section, the then-expect method provides access to the configuration.
Note that this configuration includes access to *any beans that are part of Spring application context.
As such, by using expect(consumer<Configuration>), you are able to verify if your beans executed certain operations as part of the when-phase action:
import org.axonframework.common.configuration.ApplicationConfigurer;
import org.axonframework.test.fixture.AxonTestFixture;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import static org.mockito.Mockito.*;
@SpringBootTest
class AccountSpringTest {
@Autowired
private ApplicationConfigurer configurer;
private AxonTestFixture fixture;
@BeforeEach
void setUp() {
fixture = AxonTestFixture.with(configurer);
}
@TestConfiguration
static class TestConfig {
@Bean
public MyService myService() {
return mock(MyService.class);
}
}
@Test
void testWithSpringConfigurationAndMockedBean() {
fixture.given()
.event(new AccountCreatedEvent("account-1", 500.00))
.when()
.command(new WithdrawMoneyCommand("account-1", 100.00))
.then()
.success()
.events(new MoneyWithdrawnEvent("account-1", 100.00))
.expect(config -> {
MyService myService = config.getComponent(MyService.class);
verify(myService).invoked();
});
}
@AfterEach
void tearDown() {
fixture.stop();
}
}
Tests with the Axon Server Testcontainer
Testcontainers is a library that provides lightweight, throwaway instances of databases, message brokers, and other services as Docker containers.
This makes it ideal for (integration) testing.
Axon provides a dedicated container for Axon Server, called the AxonServerContainer.
By adding this to your tests with the @Container annotation, it will be started automatically.
However, you will be required to take the host and port of the constructed container and inject them.
If you are using Spring Boot, be sure to jump to this example.
Otherwise, here’s what you need to do to use The axon-server-connector module provides the AxonServerContainer for use with Testcontainers:
import io.axoniq.axonserver.connector.AxonServerConnection;
import io.axoniq.axonserver.connector.AxonServerConnectionFactory;
import io.axoniq.axonserver.connector.impl.ServerAddress;
import jakarta.annotation.Nonnull;
import org.axonframework.axonserver.connector.AxonServerConfiguration;
import org.axonframework.common.configuration.ComponentRegistry;
import org.axonframework.common.configuration.ConfigurationEnhancer;
import org.axonframework.eventsourcing.configuration.EventSourcedEntityModule;
import org.axonframework.eventsourcing.configuration.EventSourcingConfigurer;
import org.axonframework.test.fixture.AxonTestFixture;
import org.axonframework.test.fixture.MessagesRecordingConfigurationEnhancer;
import org.axonframework.test.server.AxonServerContainer;
import org.junit.jupiter.api.*;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@Testcontainers
class AccountAxonServerTest {
@Container
static AxonServerContainer axonServer = new AxonServerContainer();
private static AxonServerConnection connection;
private AxonTestFixture fixture;
@BeforeAll
static void beforeAll() {
axonServer.start();
ServerAddress address = new ServerAddress(axonServer.getHost(), axonServer.getGrpcPort());
connection = AxonServerConnectionFactory.forClient("GameTest")
.routingServers(address)
.build()
.connect("default");
}
@BeforeEach
void setUp() {
// The MessagesRecordingConfigurationEnhancer is required to allow verification to succeed for the fixture.
// Will be resolved automatically by the test fixture combined with Spring soon.
EventSourcingConfigurer configurer =
EventSourcingConfigurer.create()
.registerEntity(EventSourcedEntityModule.autodetected(
String.class, Account.class
))
.componentRegistry(registry -> registry
.registerEnhancer(new MessagesRecordingConfigurationEnhancer())
.registerEnhancer(serverConfigurationEnhancer())
);
fixture = AxonTestFixture.with(configurer);
}
public ConfigurationEnhancer serverConfigurationEnhancer() {
return new ConfigurationEnhancer() {
@Override
public void enhance(@Nonnull ComponentRegistry registry) {
registry.registerComponent(
AxonServerConfiguration.class,
c -> {
AxonServerConfiguration serverConfig = new AxonServerConfiguration();
serverConfig.setServers(
axonServer.getHost() + ":" + axonServer.getGrpcPort()
);
return serverConfig;
}
);
}
@Override
public int order() {
return Integer.MIN_VALUE;
}
};
}
@Test
void testWithAxonServer() {
fixture.given()
.noPriorActivity()
.when()
.command(new CreateAccountCommand("account-1", 500.00))
.then()
.success()
.events(new AccountCreatedEvent("account-1", 500.00));
}
@AfterEach
void tearDown() {
fixture.stop();
}
@AfterAll
static void afterAll() {
connection.disconnect();
axonServer.stop();
}
}
As shown above, we need to inject a ConfigurationEnhancer to enhance the AxonServerConfiguration.
Axon’s configuration will pick this up and make the connection to the AxonServerContainer for message distribution and event storage.
AxonServerContainer with Spring Boot and @ServiceConnection
Spring Boot has a Testcontainers specific dependency that provides the @ServiceConnection annotation.
This annotation can be attached to Testcontainers, which then will automatically configure the connection properties for you.
Thus, if we add the @ServiceConnection to the AxonServerContainer field, our test setup is greatly simplified compared to the declarative approach:
import org.axonframework.common.configuration.ApplicationConfigurer;
import org.axonframework.test.fixture.AxonTestFixture;
import org.axonframework.test.fixture.MessagesRecordingConfigurationEnhancer;
import org.axonframework.test.server.AxonServerContainer;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@SpringBootTest
@Testcontainers
class AccountAxonServerAndSpringBootTest {
@Container
@ServiceConnection
static AxonServerContainer axonServer = new AxonServerContainer();
@Autowired
private ApplicationConfigurer configurer;
private AxonTestFixture fixture;
@TestConfiguration
static class TestConfig {
@Bean
public MessagesRecordingConfigurationEnhancer recordingConfigurationEnhancer() {
return new MessagesRecordingConfigurationEnhancer();
}
}
@BeforeEach
void setUp() {
fixture = AxonTestFixture.with(configurer);
}
@Test
void testWithAxonServer() {
fixture.given()
.noPriorActivity()
.when()
.command(new CreateAccountCommand("account-1", 500.00))
.then()
.success()
.events(new AccountCreatedEvent("account-1", 500.00));
}
@AfterEach
void tearDown() {
fixture.stop();
}
}