Как вы могли догадаться по картинке справа, речь пойдёт об автоматизированном тестировании. Точнее о такой технологии, как матчеры. Они помогают серьёзно сократить дублирование кода и упростить код тестов для восприятия, а создавать и использовать матчеры достаточно просто.
Сама по себе технология матчеров не новая — в текущем виде она была залита в репозиторий в июле 2012 года, а появилась и того раньше. Но, несмотря на это, многие о ней до сих пор не слышали или по каким-то причинам избегают. Мы хотим рассказать, как легко получать преимущества от её использования, и поделиться с вами нашей библиотекой матчеров.
Предположим, у нас есть набор неких фруктов. Среди них чаще всего встречается круглый, оранжевый и сладкий.
Известно, что именно таким условиям удовлетворяет апельсин. Помимо фруктов, у нас есть и конвейер, через который можно пропустить такие фрукты. Есть и задача для конвейера — отсеять не апельсины, проведя серию тестов.
И вот удача — у нас под рукой как раз оказался аппарат, который умеет определять сладкий ли фрукт и какого он цвета, сравнивать его форму с рядом известных и проводить еще множество проверок. Аппарат этот называется JUnit.
Перед началом теста, на конвейер вываливается новый фрукт.
Определим сперва, что фрукт круглый.
Затем, что фрукт сладкий.
И, наконец, посмотрим на его цвет.
Какие минусы сразу очевидны? Во-первых, в каждой из проверок пришлось самостоятельно склеивать комментарий из ожидаемого значения, фактического значения, значения дополнительных уточнений и разных междометий. Даже описание этого списка утомительно.
Во-вторых, представьте, что мы решили определять еще и сорт апельсинов, добавили для этого специального «дегустатора» и сказали ему: «Одобряй только сорт «Валенсия»». Но «дегустатор» не знает, что решил проверяющий автомат, если сам не спросит — автомат не болтлив. В итоге он будет пробовать все подряд. Чтобы не отравить «дегустатора» мячиком, который не успели проверить на сладость, нужно научить его игнорировать всё лишнее. Для этого ему нужно спросить автомат отдельно и самостоятельно, после чего всё забракованное отложить в сторону и больше не трогать.
«Дегустатор» — это всего лишь еще один тест в нашем JUnit-аппарате, поэтому можно и нужно использовать встроенный рантайм-механизм игнорирования теста — assume. Тогда сценарий начала дегустации будет выглядеть так.
Хорошо заметно, что у каждого нового «дегустатора» при таком описании сценария есть два пути — копипастить сценарий или делать новый метод на каждый набор предпроверок. И в каждой предпроверке снова нужно самому заботиться о составлении сообщения о причине выбраковки. Любой из этих вариантов сложно поддерживать и очень страшно воспринимать.
Вдобавок ко всему, придется отказываться от
Значит, нужно разделить логику самой проверки, вывода описания и функцию принятия решения:
Тут в дело и вступают матчеры — маленькие объекты, которые содержат логику принятия решения, знают, что ждали и что получили, о чем самостоятельно сообщают. Первые три проверки при помощи матчеров почти что поэтично описывают действие. И это может прочитать любой, кто хочет узнать, что делает сценарий.
Для такой красоты, существует специальная библиотека Hamcrest. Она содержит в себе и интерфейс для реализации, и методы assertThat и assumeThat (последний, на самом деле, внутри JUnit, но использует интерфейс из Hamcrest). Они и спрашивают матчер об объекте, принимая решение.
Начиная с версии 4.11, в зависимостях JUnit библиотека Hamcrest имеет версию не ниже 1.3. Именно она ввела интерфейс, в котором реализовано всё, что описано дальше. Поэтому, используя мавен, достаточно подключить JUnit 4.11, и минимально необходимый набор инструментов готов к использованию. А для полного набора всех доступных матчеров из поставки Hamcrest, понадобится артифакт hamcrest-all, который можно подключить отдельно.
Так может выглядеть ваш pom.
В библиотеке есть абстрактный класс
Сама по себе технология матчеров не новая — в текущем виде она была залита в репозиторий в июле 2012 года, а появилась и того раньше. Но, несмотря на это, многие о ней до сих пор не слышали или по каким-то причинам избегают. Мы хотим рассказать, как легко получать преимущества от её использования, и поделиться с вами нашей библиотекой матчеров.
Предположим, у нас есть набор неких фруктов. Среди них чаще всего встречается круглый, оранжевый и сладкий.
public class Fruit {
...
public Color getColor() {...}
public boolean isSweet() {...}
public Shape getShape() {...}
}
Известно, что именно таким условиям удовлетворяет апельсин. Помимо фруктов, у нас есть и конвейер, через который можно пропустить такие фрукты. Есть и задача для конвейера — отсеять не апельсины, проведя серию тестов.
И вот удача — у нас под рукой как раз оказался аппарат, который умеет определять сладкий ли фрукт и какого он цвета, сравнивать его форму с рядом известных и проводить еще множество проверок. Аппарат этот называется JUnit.
Перед началом теста, на конвейер вываливается новый фрукт.
@Before
public void setUp() throws Exception {
someFruit = getNextFruit();
}
Определим сперва, что фрукт круглый.
@Test
public void orangeIsRound() {
assertEquals("Expected shape - " + Shape.ROUND + ", but was - " + someFruit.getShape(),
someFruit.getShape(), Shape.ROUND);
}
Затем, что фрукт сладкий.
@Test
public void orangeIsSweet() {
assertTrue("Fruit should be sweet - expected TRUE", someFruit.isSweet());
}
И, наконец, посмотрим на его цвет.
@Test
public void orangeHasOrangeColor() {
assertEquals("Orange has orange color, but was - " + someFruit.getColor(),
someFruit.getColor(), Color.ORANGE);
}
Какие минусы сразу очевидны? Во-первых, в каждой из проверок пришлось самостоятельно склеивать комментарий из ожидаемого значения, фактического значения, значения дополнительных уточнений и разных междометий. Даже описание этого списка утомительно.
Во-вторых, представьте, что мы решили определять еще и сорт апельсинов, добавили для этого специального «дегустатора» и сказали ему: «Одобряй только сорт «Валенсия»». Но «дегустатор» не знает, что решил проверяющий автомат, если сам не спросит — автомат не болтлив. В итоге он будет пробовать все подряд. Чтобы не отравить «дегустатора» мячиком, который не успели проверить на сладость, нужно научить его игнорировать всё лишнее. Для этого ему нужно спросить автомат отдельно и самостоятельно, после чего всё забракованное отложить в сторону и больше не трогать.
«Дегустатор» — это всего лишь еще один тест в нашем JUnit-аппарате, поэтому можно и нужно использовать встроенный рантайм-механизм игнорирования теста — assume. Тогда сценарий начала дегустации будет выглядеть так.
@Test
public void degusto() {
assumeTrue("Expected shape - " + Shape.ROUND + ", but was - " + someFruit.getShape(),
someFruit.getShape().equals(Shape.ROUND));
assumeTrue("Fruit should be sweet - expected TRUE", someFruit.isSweet());
assumeTrue("Orange has orange color, but was - " + someFruit.getColor(),
someFruit.getColor().equals(Color.ORANGE));
// Далее дегустатор полностью уверен что фрукт можно кусать.
}
Хорошо заметно, что у каждого нового «дегустатора» при таком описании сценария есть два пути — копипастить сценарий или делать новый метод на каждый набор предпроверок. И в каждой предпроверке снова нужно самому заботиться о составлении сообщения о причине выбраковки. Любой из этих вариантов сложно поддерживать и очень страшно воспринимать.
Вдобавок ко всему, придется отказываться от
assertEquals
, assertNotEquals
, assertNotNull
, assertArrayEquals
и т.д. В стандартной поставке JUnit эти assert* есть почти на любой тривиальный случай. А некоторых еще и несколько — на каждый тип аргументов. То есть логика проверки заключена в названии метода и жёстко привязана к его реализации. А теперь представьте, сколько нужно было бы кода дублировать и поддерживать, если на каждый assert* пришлось бы сделать аналогичный assume*.Значит, нужно разделить логику самой проверки, вывода описания и функцию принятия решения:
- бракуем — assert,
- игнорируем — assume,
- а так же, фильтруем, ищем нужные, просеиваем, изменяем и т.д.
Тут в дело и вступают матчеры — маленькие объекты, которые содержат логику принятия решения, знают, что ждали и что получили, о чем самостоятельно сообщают. Первые три проверки при помощи матчеров почти что поэтично описывают действие. И это может прочитать любой, кто хочет узнать, что делает сценарий.
@Test
public void orangeIsRoundWithMatcher() {
assertThat(someFruit, is(round()));
}
@Test
public void orangeIsSweetWithMatcher() {
assertThat(someFruit, is(sweet()));
}
@Test
public void orangeHasColorWithMatcher() {
assertThat(someFruit, hasColor(Color.ORANGE));
}
Для такой красоты, существует специальная библиотека Hamcrest. Она содержит в себе и интерфейс для реализации, и методы assertThat и assumeThat (последний, на самом деле, внутри JUnit, но использует интерфейс из Hamcrest). Они и спрашивают матчер об объекте, принимая решение.
Начиная с версии 4.11, в зависимостях JUnit библиотека Hamcrest имеет версию не ниже 1.3. Именно она ввела интерфейс, в котором реализовано всё, что описано дальше. Поэтому, используя мавен, достаточно подключить JUnit 4.11, и минимально необходимый набор инструментов готов к использованию. А для полного набора всех доступных матчеров из поставки Hamcrest, понадобится артифакт hamcrest-all, который можно подключить отдельно.
Так может выглядеть ваш pom.
Как это работает?
В библиотеке есть абстрактный класс
TypeSafeMatcher, параметризуемый по типу проверяемого объекта. Класс предоставляет для переопределения три метода:
- public boolean matchesSafely(Fruit fruit) — логика проверки,
public void describeTo(Description description)
— описание ожидаемого значения,
-
protected void describeMismatchSafely(Fruit item, Description mismatchDescription)
— описание полученного значения.
Экземпляр класса, расширяющего этот, перед выполнением собственного кода выполнит родительский — рутинные проверки поступающего объекта на null
и соответствие указанному классу.
Например, матчер, проверяющий форму фрукта, выглядит так:
public class ShapeMatcher extends TypeSafeMatcher<Fruit> {
private Shape expected;
public ShapeMatcher(Shape expected) {
this.expected = expected;
}
@Override
public boolean matchesSafely(Fruit fruit) {
return expected.equals(fruit.getShape());
}
@Override
protected void describeMismatchSafely(Fruit item, Description mismatchDescription) {
mismatchDescription.appendText("fruit has shape - ").appendValue(item.getShape());
}
@Override
public void describeTo(Description description) {
description.appendText("shape - ").appendValue(expected);
}
@Factory
public static ShapeMatcher round() {
return new ShapeMatcher(Shape.ROUND);
}
}
Количество кода сперва пугает. Но, если приглядеться, сразу заметно, что каждое логическое действие выделено в отдельный метод, а в тесте вызов умещается в одно слово — использовать очень просто!
Но и это не все
Частая ситуация, как, например, выше, вызвана необходимостью использовать для проверки только одно свойство объекта. Целый класс для этого — лишняя трата времени и сил. Здесь на помощь приходят анонимные классы Java и абстрактный класс FeatureMatcher<WhatWeGet, WhatWeWannaCheck>
, параметризуемый двумя типами: какой объект поступит на вход и свойство какого типа нужно проверить.
Конструктор у этого класса один и требует 3 атрибута:
- матчер, который применим к WhatWeWannaCheck типу,
- описание ожидания (оно добавится к описанию субматчера),
- описание полученного значения (оно добавится к мисматч-описанию субматчера)
Потомок этого класса, переопределив метод featureValueOf
позволит вытащить нужное свойство из объекта и применить к нему существующий матчер. А их в поставке Hamcrest хватает для любых стандартных типов.
Перепишем наш матчер для формы, а заодно и остальные, используя этот класс.
public class Matchers {
public static Matcher<Fruit> hasShape(final Shape shape) {
return new FeatureMatcher<Fruit, Shape>(equalTo(shape), "fruit has shape - ", "shape -") {
@Override
protected Shape featureValueOf(Fruit fruit) {
return fruit.getShape();
}
};
}
public static Matcher<Fruit> round() {
return hasShape(Shape.ROUND);
}
public static Matcher<Fruit> sweet() {
return new FeatureMatcher<Fruit, Boolean>(is(true), "fruit should be sweet", "sweet -") {
@Override
protected Boolean featureValueOf(Fruit fruit) {
return fruit.isSweet();
}
};
}
public static Matcher<Fruit> hasColor(Color color) {
return new FeatureMatcher<Fruit, Color>(equalTo(color), "fruit have color - ", "color -") {
@Override
protected String featureValueOf(Fruit fruit) {
return fruit.getColor();
}
};
}
}
Feel the POWER OF MATCHERS
Одно из главных преимуществ матчеров — возможность их объединения. Для этого в Hamcrest есть целый ряд специальных связующих: allOf, anyOf, both, either. Каждый из них заботливо соединит и описание ожидаемого значения, и описание проваленных матчеров из цепочки.
Благодаря этому, сценарий предпроверки для нашего «дегустатора» сокращается еще сильнее:
@Test
public void orangeBothSweetRoundAndOrangeColorWithMatchers() throws Exception {
assumeThat(someFruit, both(round()).and(sweet()).and(hasColor(Color.ORANGE)));
// дальше дегустаторская магия
}
Исходники всех тестов.
Еще одна из замечательных возможностей, которую даёт эта технология, — применение одного или ряда матчеров к коллекции. Предположим, вместо одного фрукта за раз стал поступать целый набор и все объекты в нём нужно проверить одновременно. Больше не нужно никаких циклов — все проще простого:
@Test
public void orangeBothSweetRoundAndOrangeColorWithMatchers() throws Exception {
assertThat(someFruitList, everyItem(both(round()).and(sweet()).and(hasColor(Color.ORANGE))));
}
Как говорим, так и пишем: проверить каждый элемент нашим матчером. Возможны вариации — например, проверять что в пачке есть хотя бы один объект, удовлетворяющий условию hasItem()
.
Примеры работы с коллекцией.
Еще раз о велосипедах
Матчеры появились уже давно, и за всё время существования их и их наборов было создано очень много. Так что прежде чем воодушевленно писать свой матчер, поищите в интернете — скорее всего, он уже кем-то написан. Если вы его не нашли, заходите в нашу библиотеку матчеров, — возможно, там он есть. Если нужного вам матчера нигде нет и вы решили его написать, присылайте нам его реализацию в виде пулл-реквеста. Давайте вместе помогать другим не тратить время на изобретение велосипеда.
Наша библиотека матчеров находится по адресу github.com/yandex-qatools/matchers-java.