При разработке автотестов нередко приходится сталкиваться проверками 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 был взят отсюда.
Автор: p_tash
