При разработке автотестов нередко приходится сталкиваться проверками 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 был взят отсюда.
Stingray42
Зачем использовать hamcrest, когда есть assertj? Для которого тоже есть генераторы ассертов.