matcher-generator v5.1.0

For every configured type the AssertJ backend generates an assertion class, CustomerAssert for Customer. Its static assertThat returns the assertion, and each property gets a hasX method that takes the expected value. The assertion class extends AssertJ’s object assertion, so AssertJ’s own assertions stay available next to the generated ones.

The dependency is the assertj-assertion-generator artifact, which the getting started chapter declares next to the Hamcrest one.

Checking one property

A basic assertion checks one property by equality. A failing check names the property with the expected and the actual value:

CustomerAssert.assertThat(ADA).hasName("Ada");

Nested properties

A property whose type is a generated type has a hasXSatisfying method. It takes a lambda that receives the nested assertion, so the path of the failing property is reported as Customer.address.city:

CustomerAssert.assertThat(ADA)
        .hasAddressSatisfying(
                address -> AddressAssert.assertThat(address).hasCity("Paris"));

The path is bound to the value that the lambda receives, by identity. An assertion for any other object that you create inside the lambda starts at its own type, so Order.customer.address.city is reported only for the nested value itself. Two limits follow from the identity match: an unrelated assertion on null inside a lambda whose value is null inherits the path, and when the same instance is behind two properties, the path of the property whose lambda is running is reported.

A boolean property also gets isX and isNotX methods. The scope example in the completeness check section calls isActive() on the active property.

Completeness check

The completeness check fails when a property was neither checked nor ignored. Check the properties you care about, name the ones you deliberately leave out with ignoringX(), and finish with hasAllPropertiesChecked():

CustomerAssert.assertThat(ADA)
        .hasName("Ada")
        .hasAddressSatisfying(address -> AddressAssert.assertThat(address).hasCity("Paris"))
        .ignoringActive()
        .hasAllPropertiesChecked();

To run the check only inside one part of an assertion, use satisfiesCheckingAllProperties. The check covers the properties asserted in its lambda and nothing else:

CustomerAssert.assertThat(ADA)
        .satisfiesCheckingAllProperties(
                customer ->
                        customer.hasName("Ada")
                                .hasAddressSatisfying(
                                        address ->
                                                AddressAssert.assertThat(address)
                                                        .hasCity("Paris"))
                                .isActive());

Navigation with extracting(…​, as(X)) or first(as(X)) returns a new assertion for a new subject, and that assertion has its own set of checked properties. Checks you made before the navigation do not count for it, and a hasAllPropertiesChecked() after the navigation covers only the navigated type.

The whole object

Sometimes an assertion should cover the whole object, not single properties. AssertJ’s usingRecursiveComparison() does that in one step: it compares every field of the actual object with the expected one, nested objects included. Every generated assertion inherits it, so it works on CustomerAssert without any generated code:

CustomerAssert.assertThat(ADA)
        .usingRecursiveComparison()
        .isEqualTo(new Customer("Ada", new Address("1 Rue de Rivoli", "Paris"), true));

Use the generated hasX methods when a test should name the properties it checks, with a message for each. Use usingRecursiveComparison() when the expected object is at hand and every field should match. The completeness check gives the same guarantee for properties that are checked one by one, and the completeness check chapter describes it for both backends.

Printing elements in failures

When a collection assertion fails, AssertJ prints the elements with toString, and a generated class shows as Customer@1b2c3d. The AssertJ backend writes one PropertyRepresentation per target package. It prints a configured type by its properties, as Customer[name="Ada", …​]. The class is never registered automatically: register it once in the test setup with Assertions.useRepresentation, and reset AssertJ’s default with Assertions.useDefaultRepresentation(), as the example test does. The registration is global AssertJ state, shared by every test of the JVM, so a test that registers it should reset it with Assertions.useDefaultRepresentation() afterwards, as the example does in its setup:

Assertions.useRepresentation(new PropertyRepresentation());

assertThatThrownBy(
                () ->
                        Assertions.assertThat(List.of(ADA, BOB))
                                .satisfiesExactlyInAnyOrder(
                                        customer ->
                                                CustomerAssert.assertThat(customer)
                                                        .hasName("Ada"),
                                        customer ->
                                                CustomerAssert.assertThat(customer)
                                                        .hasName("Carl")))
        .hasMessageContaining("Customer[name=\"Ada\"")
        .hasMessageContaining("Customer[name=\"Bob\"");

Three limits apply to the representation:

  • It is one class per target package, and it knows only the types that the processing round writing it generated (see Files written once per package). A type generated in another compilation is not part of it and prints with toString.

  • A cyclic object graph is cut off: an object that the rendering reaches again while it is still being rendered falls back to the default rendering, so printing ends.

  • Without the registration AssertJ keeps its default rendering, for example Customer@1b2c3d.

Files written once per package

The soft assertions mixin SoftAssertionsProvider and the PropertyRepresentation exist once per package. The first processing round that writes them decides their content, and they know only the types generated in that round. A later round, for example because another processor generated a configuration class, gets a compiler warning naming the types the file does not know, and does not write the file again; a build with warnings as errors fails on it. A second compilation of the same package, such as the test compilation after the main one, writes its own mixin, and the processor prints a note when it finds a mixin of the package on the class path. Add the types to the configuration of the first round to have them in the file.

Clashing class names

Two types of one package can need the same generated class name, for example Foo and AbstractFoo both generate AbstractFooAssert. The compiler would refuse the second class, so the processor reports one error per name, writes nothing for the package and asks you to rename one of the types. Names that only resemble each other, such as FooBar and Foo_Bar, are different classes and no clash.

Next steps