DEV Community

André Schreck
André Schreck

Posted on

Testing nested structures with AssertJ

In a previous post I described why I like writing tests with AssertJ. In this post I would like to describe a nice pattern that helps testing nested structures with AssertJ.
Let’s imagin there is a Container class that consists of a name and a collection of Leaf objects.

public class Container {

    private String name;
    private List<Leaf> leaves = new ArrayList<>();

    public Container(String name, List<Leaf> leaves) {
        this.name = name;
        this.leaves.addAll(leaves);
    }

    public String getName() {
        return name;
    }

    public List<Leaf> getLeaves() {
        return Collections.unmodifiableList(leaves);
    }
}

The Leaf class consists of a name and a description.

public class Leaf {

    private String name;
    private String description;

    public Leaf(String name, String description) {
        this.name = name;
        this.description = description;
    }

    public String getName() {
        return name;
    }

    public String getDescription() {
        return description;
    }
}

For both classes an implementation of AbstractAssert can easily be implemented and could look like that.

public class ContainerAssert extends AbstractAssert<ContainerAssert, Container> {

    private ContainerAssert(Container actual) {
        super(actual, ContainerAssert.class);
    }

    public static ContainerAssert assertThat(Container actual) {
        return new ContainerAssert(actual);
    }

    public ContainerAssert hasName(String name) {
        Assertions.assertThat(actual.getName()).isEqualTo(name);
        return this;
    }
}
public class LeafAssert extends AbstractAssert<LeafAssert, Leaf> {

    private LeafAssert(Leaf actual) {
        super(actual, LeafAssert.class);
    }

    public static LeafAssert assertThat(Leaf actual) {
        return new LeafAssert(actual);
    }

    public LeafAssert hasName(String name) {
        Assertions.assertThat(actual.getName()).isEqualTo(name);
        return this;
    }

    public LeafAssert hasDescription(String description) {
        Assertions.assertThat(actual.getDescription()).isEqualTo(description);
        return this;
    }
}

The simplest way to write a test would look like that.

public class ContainerTest {

    @Test
    public void test() throws Exception {
        Container container = classUnderTest.produceContainer();

        assertThat(container).hasName("containerName");
        assertThat(container.getLeaf(0)).hasName("leaf1").hasDescription("description1");
        assertThat(container.getLeaf(1)).hasName("leaf2").hasDescription("description2");
    }
}

The thing is, that this approach does not benifit from the fluent interface of AssertJ.
The possibility of jumping back and forth between ContainerAssert and LeafAssert would be nice. To achieve this the ContainerAssert is extended by a leaf method, that returns a LeafAssert for a specific Leaf.

public class ContainerAssert extends AbstractAssert<ContainerAssert, Container> {

    private ContainerAssert(Container actual) {
        super(actual, ContainerAssert.class);
    }

    public static ContainerAssert assertThat(Container actual) {
        return new ContainerAssert(actual);
    }

    public ContainerAssert hasName(String name) {
        Assertions.assertThat(actual.getName()).isEqualTo(name);
        return this;
    }

    public LeafAssert leaf(int index) {
        return LeafAssert.assertThat(actual.getLeaves(index), actual);
    }
}

The LeafAssert also gets a new method. The parentContainer method jumps back to a ContainerAssert that corresponds to the parent of this Leaf.

public class LeafAssert extends AbstractAssert<LeafAssert, Leaf> {

    private Container currentContainer;

    private LeafAssert(Leaf actual, Container currentContainer) {
        super(actual, LeafAssert.class);
        this.currentContainer = currentContainer;
    }

    public static LeafAssert assertThat(Leaf actual) {
        return new LeafAssert(actual, null);
    }

    public static LeafAssert assertThat(Leaf actual, Container currentContainer) {
        return new LeafAssert(actual, currentContainer);
    }

    public LeafAssert hasName(String name) {
        Assertions.assertThat(actual.getName()).isEqualTo(name);
        return this;
    }

    public LeafAssert hasDescription(String description) {
        Assertions.assertThat(actual.getDescription()).isEqualTo(description);
        return this;
    }

    public ContainerAssert parentContainer() {
        return ContainerAssert.assertThat(currentContainer);
    }
}

The resulting test would look like this.

public class ContainerTest {

    @Test
    public void test() throws Exception {
        Container container = classUnderTest.produceContainer();

        assertThat(container)
            .hasName("containerName")
            .leaf(0)
            .hasName("leaf1")
            .hasDescription("description1")
            .parentContainer()
            .leaf(1)
            .hasName("leaf2")
            .hasDescription("description2");
    }
}

The problem I see with this approach is, that the LeafAssert class has to handle the currentContainer as internal state. In addition, it is not easy to see which assertion is executed on which instance.

In my opinion this problem can very elegantly be solved by passing a Consumer to the ContainerAssert’s leaf method. The Consumer accepts a LeafAssert that is initialized with the requested Leaf.

public class ContainerAssert extends AbstractAssert<ContainerAssert, Container> {

    private ContainerAssert(Container actual) {
        super(actual, ContainerAssert.class);
    }

    public static ContainerAssert assertThat(Container actual) {
        return new ContainerAssert(actual);
    }

    public ContainerAssert hasName(String name) {
        Assertions.assertThat(actual.getName()).isEqualTo(name);
        return this;
    }

    public ContainerAssert leaf(int index, Consumer<LeafAssert> consumer) {
        consumer.accept(LeafAssert.assertThat(actual.getLeaves(index)));
        return this;
    }
}

That means the LeafAssert class does not need any additional methods.

public class LeafAssert extends AbstractAssert<LeafAssert, Leaf> {

    private LeafAssert(Leaf actual) {
        super(actual, LeafAssert.class);
    }

    public static LeafAssert assertThat(Leaf actual) {
        return new LeafAssert(actual);
    }

    public LeafAssert hasName(String name) {
        Assertions.assertThat(actual.getName()).isEqualTo(name);
        return this;
    }

    public LeafAssert hasDescription(String description) {
        Assertions.assertThat(actual.getDescription()).isEqualTo(description);
        return this;
    }
}

In the test class the assertions for a specific Leaf can be passed as a lambda expression to the lead method.

public class ContainerTest {

    @Test
    public void test() throws Exception {
        Container container = classUnderTest.produceContainer();

        assertThat(container)
            .hasName("containerName")
            .leaf(0, leaf -> leaf
                .hasName("leaf1")
                .hasDescription("description1"))
            .leaf(1, leaf -> leaf
                .hasName("leaf2")
                .hasDescription("description2")));
    }
}

That way no additional state handling is necessary in the ContainerAssert and LeafAssert classes. The indentation makes it easy to see on what level of the nested structure the assertions are executed on.

Top comments (0)