При разработке автотестов нередко приходится сталкиваться проверками POJO, которые могут вернуться в ответах от API или быть сущностями в базе данных. Если вы также используете библиотеку Hamcrest, то наверняка сталкивались с проблемой, как лучше и оптимально использовать мэтчеры с POJO? Посмотрим, что предлагает Hamcrest из коробки, и познакомимся с тремя библиотеками, которые используют разные подходы.

Предположим, что есть яблочный сервис, для которого нужно написать автотест. В сервисе можно выделить следующий POJO для яблока:

public class Apple {

    private Long id;

    private String color;

    private Float weight;
    
    private String state;

    // getters; setters
}

Для примера возьмем задачу - в коллекции яблок нужно найти определенное и проверить его.

List<Apple> apples = List.of(
        new Apple()
                .setId(3L)
                .setColor("Green")
                .setWeight(70f)
                .setState("Unripe"),
        new Apple()
                .setId(2L)
                .setColor("Yellow")
                .setWeight(100f)
                .setState("Unripe"),
        new Apple()
                .setId(1L)
                .setColor("Red")
                .setWeight(120f)
                .setState("Ripe"));

Встроенные мэтчеры

Попробуем для начала обойтись возможностями, которые предлагает Hamcrest:

assertThat(apples, hasItem(allOf(
        hasProperty("id", equalTo(1L)),
        hasProperty("color", oneOf("Yellow", "Red")),
        hasProperty("weight", allOf(greaterThan(100f), lessThan(150f))),
        hasProperty("state", equalTo("Ripe")))));

Плюсы:

  • Решение "из коробки" без дополнительных библиотек

Минусы:

  • Дублируется вызов метода hasProperty

  • Ручной рефакторинг при переименовании полей

  • Отсутствует типизация, так как hasProperty знает только название поля

  • В случае несоответствия одного из полей (например, ожидаемый вес от 130 до 150) в AssertionError будет сравнение со всеми элементами коллекции:

java.lang.AssertionError: 
Expected: a collection containing (hasProperty("id", <1L>) and hasProperty("color", one of {"Yellow", "Red"}) and hasProperty("weight", (a value greater than <130.0F> and a value less than <150.0F>)) and hasProperty("state", "Ripe"))
     but: mismatches were: [hasProperty("id", <1L>)  property 'id' was <3L>, hasProperty("id", <1L>)  property 'id' was <2L>, hasProperty("weight", (a value greater than <130.0F> and a value less than <150.0F>))  property 'weight' a value greater than <130.0F> <120.0F> was less than <130.0F>]

Библиотека hamcrest-auto-matcher

Воспользуемся библиотекой hamcrest-auto-matcher (последнее обновление 2024 года). Она позволяет сравнивать POJO объекты.

Процесс добавления скрыл под спойлер

1) Добавляем в pom зависимость:

<dependency>
    <groupId>org.itsallcode</groupId>
    <artifactId>hamcrest-auto-matcher</artifactId>
    <version>0.8.2</version>
    <scope>test</scope>
</dependency>

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

assertThat(apples, hasItem(AutoMatcher.equalTo(new Apple()
        .setId(1L)
        .setColor("Red")
        .setWeight(120f)
        .setState("Ripe"))));

Плюсы:

  • Код не дублируется

Минусы:

  • Теряется вся гибкость проверок, которую дают мэтчеры в Hamcrest

  • AssertionError также сравнивает со всеми элементами в коллекции:

    java.lang.AssertionError: 
    Expected: a collection containing (id <1L> and color one of {"Yellow", "Red"} and weight (a value greater than <130.0F> and a value less than <150.0F>) and state "Ripe")
         but: mismatches were: [id <1L> id was <3L>, id <1L> id was <2L>, weight (a value greater than <130.0F> and a value less than <150.0F>) weight a value greater than <130.0F> <120.0F> was less than <130.0F>]

Библиотека Hamcrest Feature Matcher Generator for POJOs

Воспользуемся библиотекой от Яндекса feature-matcher-generator (последнее обновление 2017 года). Она позволяет генерировать отдельные мэтчеры для полей.

Процесс добавления скрыл под спойлер

1) Добавляем в pom зависимость:

<dependency>
    <groupId>ru.yandex.qatools.processors</groupId>
    <artifactId>feature-matcher-generator</artifactId>
    <version>2.0.1</version>
    <scope>provided</scope>
</dependency>

2) Добавляем в POJO аннотацию:

@GenerateMatcher
public class Apple {

    private Long id;

3) Выполняем команду mvn clean compile

4) Получаем сгенерированный класс AppleMatchers:

public final class AppleMatchers {
  /**
   * You should not instantiate this class
   */
  private AppleMatchers() {
    throw new UnsupportedOperationException("This class has only static methods");
  }

  /**
   * Matcher for {@link Apple#id}
   */
  public static Matcher<Apple> withId(Matcher<Long> matcher) {
    return new FeatureMatcher<Apple, Long>(matcher, "id", "id") {
      @Override
      public Long featureValueOf(Apple actual) {
        return actual.getId();
      }
    };
  }

  /**
   * Matcher for {@link Apple#color}
   */
  public static Matcher<Apple> withColor(Matcher<String> matcher) {
    return new FeatureMatcher<Apple, String>(matcher, "color", "color") {
      @Override
      public String featureValueOf(Apple actual) {
        return actual.getColor();
      }
    };
  }

  /**
   * Matcher for {@link Apple#weight}
   */
  public static Matcher<Apple> withWeight(Matcher<Float> matcher) {
    return new FeatureMatcher<Apple, Float>(matcher, "weight", "weight") {
      @Override
      public Float featureValueOf(Apple actual) {
        return actual.getWeight();
      }
    };
  }

  /**
   * Matcher for {@link Apple#state}
   */
  public static Matcher<Apple> withState(Matcher<String> matcher) {
    return new FeatureMatcher<Apple, String>(matcher, "state", "state") {
      @Override
      public String featureValueOf(Apple actual) {
        return actual.getState();
      }
    };
  }
}

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

assertThat(apples, hasItem(allOf(
        withId(equalTo(1L)),
        withColor(oneOf("Yellow", "Red")),
        withWeight(allOf(greaterThan(100f), lessThan(150f))),
        withState(equalTo("Ripe")))));

Плюсы:

  • Код не дублируется

  • Автоматический рефакторинг средствами IDE при переименовании полей - достаточно переименовать метод и перегенерировать мэтчеры

  • Типизация мэтчеров

Минусы:

  • При конфликте с другими полями в коде придется использовать полный импорт мэтчера, например, AppleMatchers.withId(equalTo(1L))

  • Требуется добавлять аннотацию в POJO

  • Для добавления новых полей необходимо генерировать мэтчер заново или вручную писать реализацию метода

  • AssertionError также сравнивает со всеми элементами в коллекции:

java.lang.AssertionError: 
Expected: a collection containing (id <1L> and color one of {"Yellow", "Red"} and weight (a value greater than <130.0F> and a value less than <150.0F>) and state "Ripe")
     but: mismatches were: [id <1L> id was <3L>, id <1L> id was <2L>, weight (a value greater than <130.0F> and a value less than <150.0F>) weight a value greater than <130.0F> <120.0F> was less than <130.0F>]

Библиотека Advanced Matchers

Когда столкнулся с такой проблемой, то написал решение, которое вынес в отдельную библиотеку Advanced Matchers. Она позволяет генерировать или писать мэтчеры для объектов используя минимум кода.

Процесс добавления также скрыл под спойлер

1) Добавляем в pom зависимость:

<dependency>
    <groupId>org.ptash</groupId>
    <artifactId>advanced-matchers</artifactId>
    <version>1.0</version>
    <scope>test</scope>
</dependency>

2) ЛИБО используем генератор, для этого пишем и выполняем временный скрипт:

@Test
public void generate_matchers() {
    ObjectMatcherGenerator.builder()
            .withInputPackage("org.temp.models")
            .build()
            .generate();
}

ЛИБО пишем вручную, но в любом случае на выходе получаем мэтчер, который является интерфейсом:

public interface AppleMatcher extends AdvancedMatcher<Apple> {

    static AppleMatcher appleMatcher() {
        return AdvancedMatchers.objectMatcher(Apple.class, AppleMatcher.class);
    }

    AppleMatcher id(Matcher<? super Long> matcher);

    AppleMatcher color(Matcher<? super String> matcher);

    AppleMatcher weight(Matcher<? super Float> matcher);

    AppleMatcher state(Matcher<? super String> matcher);

}

3) Добавим аннотацию над методом id с флагом identifier, который будет говорить о том, что по этому полю можно идентифицировать объект:

@ObjectMatcherField(identifier = true)
AppleMatcher id(Matcher<? super Long> matcher);

Фрагмент кода с проверкой выглядит следующим образом (метод hasItem лучше взять из класса AdvancedMatchers, а не стандартного Matchers):

assertThat(apples, hasItem(appleMatcher()
        .id(equalTo(1L))
        .color(oneOf("Yellow", "Red"))
        .weight(allOf(greaterThan(100f), lessThan(150f)))
        .state(equalTo("Ripe"))));

Плюсы:

  • Код не дублируется

  • Автоматический рефакторинг средствами IDE при переименовании полей - достаточно переименовать метод

  • Типизация мэтчеров

  • Минимум нового кода, и легко добавлять новые поля - достаточно в интерфейсе мэтчера описать метод

  • Так как нужный объект будет найден по id, то в случае несоответствия одного из полей (например, ожидаемый вес от 130 до 150) в AssertionError будут описаны только отличающиеся поля:

    java.lang.AssertionError: 
    Expected: contains with fields [id: <1L>, color: one of {"Yellow", "Red"}, weight: (a value greater than <130.0F> and a value less than <150.0F>), state: "Ripe"] minimum once
         but: mismatches were: [was with fields [weight: a value greater than <130.0F> <120.0F> was less than <130.0F>]]

Минусы:

  • Тяжело читаемые AssertionError в случае большого количества полей и объектов

  • Отсутствует прямая связь в коде между полем в POJO и его мэтчером

Заключение

Мы рассмотрели разные подходы к проверке POJO с помощью библиотеки Hamcrest. Надеюсь, это статья поможет избавить автоматизаторов от дублирования кода в проверках и уменьшить рутину написания мэтчеров.

Перечень библиотек для Hamcrest был взят отсюда.

Комментарии (1)


  1. Stingray42
    04.11.2025 12:46

    Зачем использовать hamcrest, когда есть assertj? Для которого тоже есть генераторы ассертов.