Matchers and Field Filters
When testing message handling, the fixture compares expected and actual messages using field-by-field equality.
While the built-in assertions cover most scenarios, matchers and field filters provide a more expressive test flow for the then-phase of the AxonTestFixture.
Note that neither are required, but knowing they are there and how they are used can greatly improve your tests.
Matchers
Whenever the equality validation provided by operation like events(…) or commands(…) do not cover your scenario well enough, you can use the match-based methods provided on the test fixture.
These matcher-based then-phase operations come in two flows:
-
Then-satisfies - Lambda-based verification providing the messages to assert, like
eventsSatisfy(…)andcommandsSatisfy(…). Captures thrownAssertionErrorsby the lambda to conclude the verification outcome. -
Then-matches - Predicate-based verification providing the messages to assert, like
eventsMatch(…)andcommandsMatch(…). Uses the returned boolean to conclude the verification outcome.
You are able to taking a number of courses for verification as both then-satisfies and then-matches are lambda-based. As such, you can invoke any matching logic you would like. The main difference between both, is that then-satisfies will take the verification errors as thrown by the given lambda. The then-matches flow expects a boolean to be returned to dictated if verification succeeded. In taking this route, the use of (custom) matchers can provide benefits like ignoring certain field for verification, or for drafting application specific error messages, as matchers can be tailor-made for your use case.
Built-in matchers
Axon provides several useful matchers, based on the Hamcrest Matcher interface.
These can be found in the org.axonframework.test.matchers.Matchers class as static operations, typically expecting another Matcher to complete the verification:
-
payloadsMatching(Matcher<? extends List<?>>)- Matcher unwrapping the payload of all messages, given them to the given matcher. -
messageWithPayload(Matcher<?>)- Matcher unwrapping the payload of a single message, giving it to the given Matcher. -
listWithAllOf(Matcher<T>…)- Matcher expecting a list of matchers that all must match, in any order. -
listWithAnyOf(Matcher<T>…)- Matcher expecting a list of matchers of which at least one must match, in any order. -
sequenceOf(Matcher<T>…)- Matcher expecting list of matchers that should match in the given order, allowing for gaps. -
exactSequenceOf(Matcher<T>…)- Matcher expecting list of matchers that should match exactly in the given order. -
matches(Predicate<T>)/predicate(Predicate<T>)- Matcher taking in a predicate. -
deepEquals(T expected)/deepEquals(T expected, FieldFilter filter)- Deep field-by-field quality with optional filtering. -
exactClassOf(Class<T> expected)- Exact class type matching. -
noEvents()/noCommands()- Empty collection matching. -
andNoMore()/nothing()- Matcher signaling the end, to be used when a list of matchers is expected.
The code samples below show a number of examples using the built-in matchers. Note that all of them use Axon’s then-satisfies flow in combination with Hamcrest’s assertThat to contain the built-in matcher:
-
Payloads matching a listWithAllOf() containing matches()
-
Exact sequence of messageWithPayload() containing exactClassOf() and no more
-
No events
import org.axonframework.common.configuration.ApplicationConfigurer;
import org.axonframework.test.fixture.AxonTestFixture;
import org.junit.jupiter.api.*;
import static org.axonframework.test.matchers.Matchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
class AccountTest {
private AxonTestFixture fixture;
@Test
void listWithAllOfMatchers() {
fixture.when()
.command(new ProcessOrderCommand("order-1"))
.then()
.eventsSatisfy(events -> assertThat(events, payloadsMatching(
listWithAllOf(
matches(payload -> payload instanceof OrderProcessedEvent),
matches(payload -> payload instanceof InventoryIncrementedEvent)
)
)));
}
}
import org.axonframework.common.configuration.ApplicationConfigurer;
import org.axonframework.test.fixture.AxonTestFixture;
import org.junit.jupiter.api.*;
import static org.axonframework.test.matchers.Matchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
class AccountTest {
private AxonTestFixture fixture;
@Test
void exactSequenceOfExactClasses() {
fixture.when()
.command(new CompleteOrderCommand("order-1"))
.then()
.eventsSatisfy(events -> assertThat(events, exactSequenceOf(
messageWithPayload(exactClassOf(OrderCompletedEvent.class)),
messageWithPayload(exactClassOf(PaymentProcessedEvent.class)),
andNoMore()
)));
}
}
import org.axonframework.common.configuration.ApplicationConfigurer;
import org.axonframework.test.fixture.AxonTestFixture;
import org.junit.jupiter.api.*;
import static org.axonframework.test.matchers.Matchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
class AccountTest {
private AxonTestFixture fixture;
@Test
void noEvents() {
fixture.when()
.command(new CompleteOrderCommand("unknown"))
.then()
.eventsSatisfy(events -> assertThat(events, Matchers.noEvents()));
}
}
Field filters
Field filters control which fields are included in this comparison, allowing you to ignore non-deterministic fields like timestamps and UUIDs. To explain the benefits of field filters, let us look at the following example event:
public record AccountCreatedEvent(
String accountId,
double initialBalance
) {
}
When testing, you want to verify the initialBalance, but not the accountId since it is generated at runtime.
Hence, it will differ for every run.
As such, a plain test as shown below would not work:
import org.axonframework.test.fixture.AxonTestFixture;
class AccountTest {
private AxonTestFixture fixture;
@Test
void test() {
fixture.given()
.noPriorActivity()
.when()
.command(new CreateAccountCommand(500.00))
.then()
.events(new AccountCreatedEvent("???", 500.00));
// Verify fails as we are unaware of the accountId
}
}
The field filter solves this problem by instructing the fixture that certain fields can be "filtered" from verification. Any registered field filters are applied during comparison in the then-phase following these steps:
-
The fixture recursively examines all fields of expected and actual (nested) objects.
-
For each field, all registered field filters are consulted.
-
If any filter returns
false, the field is skipped. -
Only fields that pass all filters are compared.
Before we look at registering field filters and common examples, it is good to know the alternatives. This will allow you to take the most reasonable course of action for your application.
-
The flexibility of matchers can resolve filtering on a per-test basis.
-
Inject mockable services that generate the fields you need to ignore. This provides control over the returned result and thus the verified result. Hence, field filters would become obsolete when services can be mocked.
Registering field filters
To register field filters with the test fixture, you need to customize it.
Hence, instead of using AxonTestFixture#with(ApplicationConfigurer), you use the AxonTestFixture#with(ApplicationConfigurer, UnaryOperator<Customization>) operation.
The lambda on the Customization provides two methods to register field filters, which are:
-
Customization#registerIgnoredField(Class<?> declaringClass, String fieldName) -
Customization#registerFieldFilter(FieldFilter)
Thus, if you want to ignore a field for a specific message, you can use the registerIgnoredField operation:
import org.axonframework.common.configuration.ApplicationConfigurer;
import org.axonframework.test.fixture.AxonTestFixture;
import org.junit.jupiter.api.*;
class AccountTest {
private ApplicationConfigurer configurer;
private AxonTestFixture fixture;
@BeforeEach
void setUp() {
fixture = AxonTestFixture.with(
configurer,
customization -> customization.registerIgnoredField(
AccountCreatedEvent.class, "accountId"
)
);
}
}
With this filter in place, the fixture will ignore fields named "accountId" for messages with the payload type AccountCreatedEvent.
If you need more control, you can provide a (custom) FieldFilter through the registerFieldFilter method:
import org.axonframework.common.configuration.ApplicationConfigurer;
import org.axonframework.test.fixture.AxonTestFixture;
import org.junit.jupiter.api.*;
class AccountTest {
private ApplicationConfigurer configurer;
private AxonTestFixture fixture;
@BeforeEach
void setUp() {
fixture = AxonTestFixture.with(
configurer,
customization -> customization.registerFieldFilter(
field -> !field.getName().equals("accountId")
)
);
}
}
The above example will filter out all occurrences of fields name "accountId" regardless of the message type.
Note that the FieldFilter is a functional interface:
@FunctionalInterface
public interface FieldFilter {
boolean accept(Field field);
}
Returning true will include the field in comparison.
Thus false will ignore it.
Built-in field filters
Axon provides several built-in field filters that you can use instead of constructing your own:
-
AllFieldsFilter- Accepts all fields (the default behavior). -
NonStaticFieldsFilter- Excludes static fields from comparison. -
NonTransientFieldsFilter- Excludes transient fields from comparison. -
IgnoreField- Ignores a specific field; this is whatregisterIgnoredField()uses internally. -
MatchAllFieldFilter- Combines multiple filters using AND logic (all filters must accept the field).
Example field filter usages
The tabs below show a couple of common scenarios for which you could use field filters in your test fixtures,
-
Filter timestamp fields
-
Filter identifier fields
-
Filter annotated fields
-
Combining filters
This custom FieldFilter registered below will ignore fields that are named "timestamp", "createdate", or "updatedate", and if the type is an Instant.
import org.axonframework.common.configuration.ApplicationConfigurer;
import org.axonframework.test.fixture.AxonTestFixture;
import java.time.Instant;
class AccountTest {
private ApplicationConfigurer configurer;
private AxonTestFixture fixture;
@BeforeEach
void setUp() {
fixture = AxonTestFixture.with(
configurer,
customization -> customization.registerFieldFilter(field -> {
String name = field.getName();
return !(name.contains("timestamp")
|| name.contains("createDate")
|| name.contains("updateDate"))
&& !field.getType().equals(Instant.class);
})
);
}
}
This custom FieldFilter registered below will ignore fields that are contain "id" in the name and are of type UUID.
import org.axonframework.common.configuration.ApplicationConfigurer;
import org.axonframework.test.fixture.AxonTestFixture;
import org.junit.jupiter.api.*;
import java.util.UUID;
class AccountTest {
private ApplicationConfigurer configurer;
private AxonTestFixture fixture;
@BeforeEach
void setUp() {
fixture = AxonTestFixture.with(
configurer,
customization -> customization.registerFieldFilter(
field -> !field.getType().equals(UUID.class)
&& !field.getName().toLowerCase().contains("id")
)
);
}
}
You can filter on annotations as well, since the FieldFilter receive a Field.
Thus, you can define a custom annotation to mark fields that should be ignored.
In the example below, we have constructed an @IgnoreInTest annotation that we filter on.
import org.axonframework.common.configuration.ApplicationConfigurer;
import org.axonframework.test.fixture.AxonTestFixture;
import org.junit.jupiter.api.*;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface IgnoreInTest {
// Annotation is named as-s for example purposes only!
}
record AccountCreatedEvent(
@IgnoreInTest String accountId,
double amount
) {
}
class AccountTest {
private ApplicationConfigurer configurer;
private AxonTestFixture fixture;
@BeforeEach
void setUp() {
fixture = AxonTestFixture.with(
configurer,
customization -> customization.registerFieldFilter(
field -> !field.isAnnotationPresent(IgnoreInTest.class)
)
);
}
}
Combine multiple filters. All filters are combined with AND logic - a field must pass all filters to be included.
import org.axonframework.common.configuration.ApplicationConfigurer;
import org.axonframework.test.fixture.AxonTestFixture;
import org.axonframework.test.matchers.NonTransientFieldsFilter;
import org.junit.jupiter.api.*;
class AccountTest {
private ApplicationConfigurer configurer;
private AxonTestFixture fixture;
@BeforeEach
void setUp() {
fixture = AxonTestFixture.with(configurer, AccountTest::registerFilters);
}
private static AxonTestFixture.Customization registerFilters(
AxonTestFixture.Customization customization
) {
return customization.registerIgnoredField(AccountCreatedEvent.class, "accountId")
.registerIgnoredField(OrderCreatedEvent.class, "orderId")
.registerFieldFilter(NonTransientFieldsFilter.instance())
.registerFieldFilter(field -> !field.getName().contains("internal"));
}
}