0

I was trying to create a unit test which would make sure a list (or, more generally, a container) contains certain mandatory items, while allowing it to also contain some extra optional items (but, again, from a pre-defined list of options).

Let's assume, for definiteness, that the list:

  1. must contain items foo and bar;
  2. may contain item optional;
  3. may not contain any other items.

In Java, using a handy satisfiesAnyOf() function from the AssertJ library, this test can be written as follows:

import static java.util.Arrays.asList;
import static org.assertj.core.api.Assertions.assertThat;

import java.util.List;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

@RunWith(JUnit4.class)
public class C {
    @Test
    public void t0() {
        doTest(asList("foo", "bar"));
    }

    @Test
    public void t1() {
        doTest(asList("foo", "bar", "optional"));
    }

    private static void doTest(final List<String> items) {
        assertThat(items).as("items").satisfiesAnyOf(
                it -> assertThat(it).as("XXX").containsExactlyInAnyOrder("foo", "bar"),
                it -> assertThat(it).as("YYY").containsExactlyInAnyOrder("foo", "bar", "optional"));
    }
}

The problem with this code is that it only compiles using a Java 8 compiler, with Java 9+ compilers rejecting it with:

C.java:[26,63] no suitable method found for containsExactlyInAnyOrder(java.lang.String,java.lang.String)
    method org.assertj.core.api.AbstractIterableAssert.containsExactlyInAnyOrder(capture#1 of ? extends java.lang.String...) is not applicable
      (varargs mismatch; java.lang.String cannot be converted to capture#1 of ? extends java.lang.String)
    method org.assertj.core.api.ListAssert.containsExactlyInAnyOrder(capture#1 of ? extends java.lang.String...) is not applicable
      (varargs mismatch; java.lang.String cannot be converted to capture#1 of ? extends java.lang.String)
C.java:[27,63] no suitable method found for containsExactlyInAnyOrder(java.lang.String,java.lang.String,java.lang.String)
    method org.assertj.core.api.AbstractIterableAssert.containsExactlyInAnyOrder(capture#2 of ? extends java.lang.String...) is not applicable
      (varargs mismatch; java.lang.String cannot be converted to capture#2 of ? extends java.lang.String)
    method org.assertj.core.api.ListAssert.containsExactlyInAnyOrder(capture#2 of ? extends java.lang.String...) is not applicable
      (varargs mismatch; java.lang.String cannot be converted to capture#2 of ? extends java.lang.String)

The client code can easily be fixed by explicitly casting it in each lambda from List<? extends String> to List<String> which doesn't even change the generated bytecode:

        assertThat(items).as("items").satisfiesAnyOf(
                it -> assertThat((List<String>) it).as("XXX").containsExactlyInAnyOrder("foo", "bar"),
                it -> assertThat((List<String>) it).as("YYY").containsExactlyInAnyOrder("foo", "bar", "optional"));

Looking at AssertJ API, I can't figure out how exactly the above issue can be addressed w/o unsafe casts.

Speaking in favour of Kotlin, the equivalent Kotlin code compiles just fine (the type of it can be specified as either MutableList<out String> or List<String>):

import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4

@RunWith(JUnit4::class)
class K {
  @Test
  fun t() {
    val items = listOf("foo", "bar", "optional")

    assertThat(items).`as`("items").satisfiesAnyOf({ assertThat(it).`as`("XXX").containsExactlyInAnyOrder("foo", "bar") },
                                                   { assertThat(it).`as`("YYY").containsExactlyInAnyOrder("foo", "bar", "optional") })
  }
}

Questions:

  1. What exactly has changed in the JLS between Java 8 and Java 9? Can you point me to the exact section of the specification which forbids successful compilation?
  2. How can the above Java code be rewritten to avoid explicit type casts?
Bass
  • 4,977
  • 2
  • 36
  • 82
  • NB: it's better to do `(List it) -> assertThat(...`, rather than casting. – Andy Turner May 26 '20 at 22:25
  • @AndyTurner Indeed, but this wouldn't compile. The type of the lambda expected by `satisfiesAnyOf()` should be `Consumer>`. – Bass May 26 '20 at 22:32

1 Answers1

0

What version of AssertJ are you using?

There is a bug solved with Java 9 in AssertJ Core 3.15.0. I'd give it a go with 3.16 and see how it goes if you haven't tried it.

Vargan
  • 1,277
  • 1
  • 11
  • 35
  • It's 3.16.1, the latest released one. You can grab the project here: https://github.com/unix-junkie/assertj-variance-issue – Bass May 26 '20 at 22:28