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, Axon Framework builds and starts the AxonConfiguration for you as part of the application context lifecycle. In your tests, you can inject this configuration directly and use it to create an AxonTestFixture.

Note that the MessagesRecordingConfigurationEnhancer must be registered as a bean so that the fixture can capture dispatched commands and published events.

import org.axonframework.common.configuration.AxonConfiguration;
import org.axonframework.test.fixture.AxonTestFixture;
import org.axonframework.test.fixture.MessagesRecordingConfigurationEnhancer;
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;

@SpringBootTest
class AccountSpringTest {

    @TestConfiguration
    static class TestConfig {

        @Bean
        public MessagesRecordingConfigurationEnhancer recordingEnhancer() {
            return new MessagesRecordingConfigurationEnhancer();
        }
    }

    @Autowired
    private AxonConfiguration configuration;

    private AxonTestFixture fixture;

    @BeforeEach
    void setUp() {
        fixture = new AxonTestFixture(configuration, new AxonTestFixture.Customization());
    }

    @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.

Using @AxonSpringBootTest

The @AxonSpringBootTest annotation simplifies Spring Boot testing by eliminating the manual fixture setup shown above. It automatically:

  • Registers the MessagesRecordingConfigurationEnhancer (required for the fixture to capture commands and events).

  • Exposes an AxonTestFixture bean that can be injected directly.

  • Manages the fixture lifecycle (no @BeforeEach / @AfterEach needed).

To use it, add the axon-spring-boot-starter-test dependency:

<dependency>
    <groupId>org.axonframework.extensions.spring</groupId>
    <artifactId>axon-spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

Then annotate your test class:

import org.axonframework.extension.springboot.test.AxonSpringBootTest;
import org.axonframework.test.fixture.AxonTestFixture;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;

@AxonSpringBootTest(properties = "axon.axonserver.enabled=false")
class AccountSpringBootTest {

    @Autowired
    private AxonTestFixture fixture;

    @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));
    }
}

The fixture’s Customization is derived automatically from the Spring environment. When axon.axonserver.enabled is false, the fixture disables Axon Server; when true or absent, it remains enabled.

To override the default customization, declare a Customization bean in the test:

import org.axonframework.extension.springboot.test.AxonSpringBootTest;
import org.axonframework.test.fixture.AxonTestFixture;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;

@AxonSpringBootTest
class AccountCustomizedTest {

    @TestConfiguration
    static class TestConfig {

        @Bean
        public AxonTestFixture.Customization customization() {
            return new AxonTestFixture.Customization().disableAxonServer();
        }
    }

    @Autowired
    private AxonTestFixture fixture;

    @Test
    void testWithCustomization() {
        fixture.given()
               .noPriorActivity()
               .when()
               .command(new CreateAccountCommand("account-1", 500.00))
               .then()
               .success()
               .events(new AccountCreatedEvent("account-1", 500.00));
    }
}

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.extension.springboot.test.AxonSpringBootTest;
import org.axonframework.test.fixture.AxonTestFixture;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;

import static org.mockito.Mockito.*;

@AxonSpringBootTest(properties = "axon.axonserver.enabled=false")
class AccountSpringTest {

    @TestConfiguration
    static class TestConfig {

        @Bean
        public MyService myService() {
            return mock(MyService.class);
        }
    }

    @Autowired
    private AxonTestFixture fixture;

    @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();
               });
    }
}

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.extension.springboot.test.AxonSpringBootTest;
import org.axonframework.test.fixture.AxonTestFixture;
import org.axonframework.test.server.AxonServerContainer;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

@AxonSpringBootTest
@Testcontainers
class AccountAxonServerAndSpringBootTest {

    @Container
    @ServiceConnection
    static AxonServerContainer axonServer = new AxonServerContainer();

    @Autowired
    private AxonTestFixture fixture;

    @Test
    void testWithAxonServer() {
        fixture.given()
               .noPriorActivity()
               .when()
               .command(new CreateAccountCommand("account-1", 500.00))
               .then()
               .success()
               .events(new AccountCreatedEvent("account-1", 500.00));
    }
}