What is the captured class of the satisfies method when applied to a list?

168 views Asked by At

I admit this is a contrived situation, but why does the following code not compile?

Environment details:

  • Java: openjdk 11.0.20 2023-07-18
  • AssertJ: 3.24.2 (discovered while running 3.21.0)
package example;

import org.junit.Test;

import java.util.List;

import static org.assertj.core.api.Assertions.*;

public class ExampleTest {
    public interface ICommon {

    }

    public static class TypeA implements ICommon {
        // assume overridden equals
    }

    public static class TypeB implements ICommon {
        // assume overridden equals
    }

    @Test
    public void test() {
        final List<ICommon> actual = List.of(new TypeA(), new TypeB());
        final List<ICommon> expected = List.of(new TypeA());

        final Iterable<? extends ICommon> thisCompiles = expected; // no problems

        /*
         * 'containsAnyElementsOf(java.lang.Iterable<? extends capture<? extends example.ExampleTest.ICommon>>)'
         * in 'org.assertj.core.api.AbstractIterableAssert' cannot be applied to
         * '(java.util.List<example.ExampleTest.ICommon>)'
         */
        assertThat(actual).satisfies(r -> assertThat(r).containsAnyElementsOf(expected));

        /*
         *
         * 'containsAnyElementsOf(java.lang.Iterable<? extends capture<? extends example.ExampleTest.ICommon>>)'
         * in 'org.assertj.core.api.AbstractIterableAssert' cannot be applied to
         * '(java.lang.Iterable<capture<? extends example.ExampleTest.ICommon>>)'
         */
        assertThat(actual).satisfies(r -> assertThat(r).containsAnyElementsOf(thisCompiles));
    }
}

(I Reiterate: this example is not a good way to test what is being tested, it is merely a stripped down model for example purposes only. DO NOT use it as an example of best practices. This is not a place of honor.)

A coworker came to me with code that ultimately boiled down to a similar situation. I managed to give them a better way to do the assertion, but I was personally disturbed because I realized I could not explain why this code does not compile. Because of this I am returning to StackOverflow after a long absence to throw this to the community. What is preventing the compiler from figuring this code out?

1

There are 1 answers

11
Mr. Polywhirl On

You need to assign the assertThat(assertion) statement to a typed ListAssert<ICommon> variable. This is because the assertThat method does not know what the concrete list type should be.

assertThat(actual).satisfies(assertion -> {
    ListAssert<ICommon> list = assertThat(assertion); // Declare the type
    list.containsAnyElementsOf(expected);
});

Here is a working unit test. I used Lombok to annotate the classes to supply the equals() and hashCode() methods. Also, I changed List.of to Arrays.asList for Java 8 compatibility. Feel free to change it back.

package org.example;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.Arrays;
import java.util.List;

import org.assertj.core.api.ListAssert;
import org.junit.Test;

import lombok.EqualsAndHashCode;

public class ExampleTest {
    public interface ICommon {}

    @EqualsAndHashCode
    public static class TypeA implements ICommon {}

    @EqualsAndHashCode
    public static class TypeB implements ICommon {}

    @Test
    public void test() {
        final List<ICommon> actual = Arrays.asList(new TypeA(), new TypeB());
        final List<ICommon> expected = Arrays.asList(new TypeA());

        assertThat(actual).satisfies(assertion -> {
            ListAssert<ICommon> list = assertThat(assertion);
            list.containsAnyElementsOf(expected);
        });
    }
}

You could also cast the list inline:

@Test
@SuppressWarnings("unchecked")
public void test() {
    final List<ICommon> actual = Arrays.asList(new TypeA(), new TypeB());
    final List<ICommon> expected = Arrays.asList(new TypeA());

    assertThat(actual)
        .satisfies(assertion -> ((ListAssert<ICommon>) assertThat(assertion))
            .containsAnyElementsOf(expected));
}

Since the org.assertj.core.api.Assertions#assertThat method is static, you are required to assign its result into a ListAssert<ELEMENT> variables. Where ELEMENT is the type of list item to be checked. I believe they thing that is throwing you off is the <? extends ELEMENT> part.

/**
 * Creates a new instance of <code>{@link ListAssert}</code>.
 *
 * @param <ELEMENT> the type of elements.
 * @param actual the actual value.
 * @return the created assertion object.
 */
public static <ELEMENT> ListAssert<ELEMENT> assertThat(List<? extends ELEMENT> actual) {
  return AssertionsForInterfaceTypes.assertThat(actual);
}

You could simplify this to just:

assertThat(actual).containsAnyElementsOf(expected);