3

I've stumbled on an issue where AssertJ generates the following code in one of the assertion classes:

public S hasItems(interface ItemInterface... items)

This of course doesn't compile.

An example code that causes the problem is as follows:


    public interface EntityInterface {

      Set<? extends ItemInterface> getItems();
    }

    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    @With
    public class EntityA implements EntityInterface {

      private Set<ItemA> items;
    }

    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    @With
    public class EntityA implements EntityInterface {

      private Set<ItemA> items;
    }

    public interface ItemInterface {

      String getName();
    }

    public class ItemA implements ItemInterface {

      public String getName() {
        return "ItemA";
      }
    }

    public class ItemA implements ItemInterface {

      public String getName() {
        return "ItemA";
      }
    }

I've included the minimum example project that causes this error, so it can be seen firsthand. It can be downloaded from filebin

We're using Lombok's @With annotation among other considerations and need to keep the interfaces.

To fix this, I have tried:

  1. Changing the getItems method signature to:

<T extends ItemInterface> Set<T> getItems();

which produces:

public S hasItems(T... items)

however T is not known in the context.

  1. Turning the inteface into a template using:

public interface EntityInterface<T extends ItemInterface>

which didn't make any difference.

Is there a solution that I'm missing?

2 Answers2

3

I have given a good try to support generics in the https://github.com/joel-costigliola/assertj-assertions-generator, it turned out that the problem is quite complex and I unfortunately had to give up due to lack of good solution and other priorities :(.

My best answer is that the generator is just a way to quickly get your custom assertions, it's not meant to be perfect so once generated change them to your need - read http://joel-costigliola.github.io/assertj/assertj-assertions-generator.html#philosophy.

I hope my answer is not too disappointing.

Joel Costigliola
  • 6,308
  • 27
  • 35
  • Thank you for the anwer! It's a shame but templates do tend to make things rather complex. I decided to add the interface to the excluded list and work around it. – user8682752 Dec 02 '19 at 11:37
  • 1
    Good to know! WHat I usually tend to do is modify the generated assertions to get them more domain oriented. So the generator is really a way to get quickly a bunch of assertions that I tweak later on. – Joel Costigliola Dec 03 '19 at 02:30
1

As you have observed, the problem is that AssertJ creates AbstractEntityInterfaceAssert class with invalid methods like:

public S hasItems(interface ItemInterface... items) {/*...*/}

I have no practical experience with AssertJ but after some research I came to 2 workarounds where compile-time type safety is preserved (changing EntityInterface.getItems() method to return Set<?> works, but is unacceptable):

  1. Use abstract class which implements the interface, instead of using the interface directly:
public interface ItemInterface {
  String getName();
}

public abstract class AbstractItem implements ItemInterface {
}

public class ItemA extends AbstractItem {
  public String getName() {
    return "ItemA";
  }
}

// ItemB same as ItemA

public interface EntityInterface {
  Set<? extends AbstractItem> getItems();
}

@NoArgsConstructor
@AllArgsConstructor
@Data
@With
public class EntityA implements EntityInterface {
  private Set<ItemA> items;
}

// EntityB same as EntityA, but with Set of ItemB

As can be seen, the only changes compared to your example is that AbstractItem is used as base class instead of implementing ItemInterface in both ItemA and ItemA and EntityInterface.getItems() method is changed to return Set<? extends AbstractItem> instead of Set<? extends ItemInterface>.

With these changes the program compiles correctly and generated AbstractEntityInterfaceAssert class has valid method signatures like:

public S hasItems(AbstractItem... items) { /*...*/ }
  1. The second workaround is to exclude EntityInterface from generation using assertj-assertions-generator-maven-plugin settings:
    <build>
        <plugins>
            <plugin>
                <groupId>org.assertj</groupId>
                <artifactId>assertj-assertions-generator-maven-plugin</artifactId>
                <!-- ... -->
                <configuration>
                    <!-- ... -->
                    <!-- Exclude classes matching the regex from generation -->
                    <excludes>
                        <param>com.example.EntityInterface</param>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

The above configuration prevents generation of AbstractEntityInterfaceAssert.java.

I don't know if any of these workarounds is applicable to your use case and unfortunately I cannot provide a better solution or explanation (is that a bug or a limitation of AspectJ?). The best person for that is Joel Costigliola - the author of AssertJ

Helpful reads:

MartinBG
  • 1,500
  • 13
  • 22
  • 1
    Thank you for the extensive answer! I went with the second option in the end, because the similarity between the items is not great enough for an abstract class to make sense semantically. The downside is, that the AssertJ methods are not available when dealing with the interface, but perhaps that's for the best :) – user8682752 Dec 02 '19 at 11:35