AssertJ: генерация быстрых утверждений для Set‹A extends B›

Я наткнулся на проблему, когда AssertJ генерирует следующий код в одном из классов утверждений:

public S hasItems(interface ItemInterface... items)

Это, конечно, не компилируется.

Пример кода, который вызывает проблему, выглядит следующим образом:


    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";
      }
    }

Я включил минимальный пример проекта, который вызывает эту ошибку, чтобы его можно было увидеть из первых рук. Его можно загрузить из filebin.

Мы используем аннотацию Lombok @With среди других соображений и должны сохранить интерфейсы.

Чтобы исправить это, я пробовал:

  1. Изменение подписи метода getItems на:

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

который производит:

public S hasItems(T... items)

однако T не известен в контексте.

  1. Превращение интерфейса в шаблон с помощью:

public interface EntityInterface<T extends ItemInterface>

что не имело никакого значения.

Есть ли решение, которое мне не хватает?


person Alan P.    schedule 29.11.2019    source источник


Ответы (2)


Как вы заметили, проблема в том, что AssertJ создает класс AbstractEntityInterfaceAssert с недопустимыми методами, такими как:

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

У меня нет практического опыта работы с AssertJ, но после некоторых исследований я пришел к двум обходным путям, где тип времени компиляции безопасность сохраняется (изменение метода EntityInterface.getItems() на возврат Set<?> работает, но неприемлемо):

  1. Используйте абстрактный класс, который реализует интерфейс, вместо того, чтобы использовать интерфейс напрямую:
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

Как видно, единственное изменение по сравнению с вашим примером заключается в том, что AbstractItem используется в качестве базового класса вместо реализации ItemInterface как в ItemA, так и в ItemA, а метод EntityInterface.getItems() изменен, чтобы возвращать Set<? extends AbstractItem> вместо Set<? extends ItemInterface>.

С этими изменениями программа компилируется правильно, и сгенерированный класс AbstractEntityInterfaceAssert имеет допустимые сигнатуры методов, такие как:

public S hasItems(AbstractItem... items) { /*...*/ }
  1. Второй обходной путь — исключить EntityInterface из генерации с помощью assertj-assertions-generator-maven-plugin настроек. :
    <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>

Приведенная выше конфигурация предотвращает создание AbstractEntityInterfaceAssert.java.

Я не знаю, применим ли какой-либо из этих обходных путей к вашему варианту использования, и, к сожалению, я не могу предоставить лучшее решение или объяснение (это ошибка или ограничение AspectJ?). Лучше всего для этого подходит Джоэл Костильола - автор AssertJ.

Полезно читать:

person MartinBG    schedule 30.11.2019
comment
Спасибо за развернутый ответ! В конце концов я выбрал второй вариант, потому что сходство между элементами недостаточно велико, чтобы абстрактный класс имел семантический смысл. Минус в том, что методы AssertJ недоступны при работе с интерфейсом, но, возможно, это и к лучшему :) - person Alan P.; 02.12.2019

Я хорошо постарался поддержать дженерики в https://github.com/joel-costigliola/assertj-assertions-generator, оказалось, что проблема довольно сложная и мне, к сожалению, пришлось сдаться из-за отсутствия хорошего решения и других приоритетов :(.

Мой лучший ответ заключается в том, что генератор — это просто способ быстро получить ваши собственные утверждения, он не должен быть идеальным, поэтому после создания измените их в соответствии с вашими потребностями — прочитайте http://joel-costigliola.github.io/assertj/assertj-assertions-generator.html#philosophy.

Надеюсь, мой ответ не слишком разочаровывает.

person Joel Costigliola    schedule 30.11.2019
comment
Спасибо за ответ! Это позор, но шаблоны имеют тенденцию делать вещи довольно сложными. Я решил добавить интерфейс в список исключений и обойти его. - person Alan P.; 02.12.2019
comment
Хорошо знать! Что я обычно делаю, так это модифицирую сгенерированные утверждения, чтобы сделать их более ориентированными на предметную область. Таким образом, генератор — это действительно способ быстро получить кучу утверждений, которые я позже настрою. - person Joel Costigliola; 03.12.2019