Работа с Hamcrest

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

Что такое Hamcrest

Hamcrest помогает писать тесты. Не надо путать его с полнофункциональными фреймворками для тестирования, такими как JUnit. Hamcrest – это всего лишь библиотека matcher-ов, которая используется в паре с JUnit или другим аналогичным фреймворком для тестирования.

Название Hamcrest является анаграммой «matcher». Matcher — это такое выражение, тестирующее на совпадение с определенным условием. Возьмем простейший пример на Hamcrest, тестирующий совпадение двух строк:

String string = "Petya";
assertThat(string, equalTo("Petya"));

Здесь «matcher»-ом является equalTo(). Функция же assertThat() — это единственный доступный предикат в библиотеке Hamcrest.

В Hamcrest вся функциональность переложена именно на matcher-ы, такие как equalTo(), not(), anyOf(), is(). Их много, и они разного предназначения: логические, для тестирования объектов и бинов, для тестирования строк, чисел, коллекций.

А предикат assertThat() всего один, именно он используется для тестирования всех объектов.

Hamcrest вместе с JUnit

Дополним предыдущий пример аннотациями JUnit – сделаем из него полноценный тест, который можно запускать:

@Test
public void givenString_whenEqual_thenCorrect() {
    String string = "Petya";
    assertThat(string, equalTo("Petya"));
}

Здесь аннотация @Test относится к пакету org.junit. И весь тест запускается с помощью JUnut Runner.

Теперь должно быть понятно, где заканчивается библиотека Hamcrest и начинается другой тестировочный фреймворк, в нашем случае это JUnit.

Преимущества Hamcrest

Зачем же нужен Hamcrest, если есть assert-ы JUnit? В Hamcrest составлять проверки удобнее, а главное, они более читаемые.

Возвращаемые тексты ошибок тоже скажут больше о проблеме.

Вообще на данный момент известны несколько поколений assert, появившихся до Hamcrest:

  • В первом поколении использовался единственный предикат assert(boolean b) с единственным аргументом, возвращающим true либо false. Читать такие условия было довольно трудно.
  • Во втором поколении появились вариации предикатов: assertEquals(), assertTrue() и многие другие. Их стало читать уже проще, различных assert… было много.
  • В Hamcrest снова используется единственный предикат – assertThat(). Но зато появилось много matcher-ов для проверки условий, добавляющих функциональность и читабельность выражениям. Эти matcher-ы мы и рассмотрим ниже.

Но сначала надо настроить проект, добавив в него библиотеку Hamcrest. Мы собираем проект на Maven.

Maven зависимость

Добавьте в pom.xml библиотеку hamrest-all:

<dependency>
    <groupId>org.hamcrest</groupId>
    <artifactId>hamcrest-all</artifactId>
    <version>1.3</version>
    <scope>test</scope>
</dependency>

Последнюю версию зависимости вы найдете здесь.

Статический импорт

Также вначале класса необходимо сделать статический импорт. Не забудьте его сделать заранее, поскольку Eclipse не выдает подсказок для статических методов, а все матчеры являются статическими методами. Так что без импорта всех методов вы не сможете с помощью Ctrl+Enter посмотреть подсказки.

Чтобы подсказки были, надо импортировать все matcher сразу, поставив звездочку:

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;

Пример использования

Итак, полностью пример выглядит так:

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;

import org.junit.Test;

public class HamcrestFirstTest {
    @Test
    public void givenString_whenEqual_thenCorrect() {
        String string = "Petya";
        assertThat(string, equalTo("Petya"));
    }
}

Обратите внимание, мы используем assertThat () с двумя аргументами. Первым аргументом идет проверяемый объект, а вторым – matcher, то есть условие проверки.

Предикат с одним аргументом assertThat(boolean b) тоже существует, но чем он лучше assert(boolean b) первого поколения? Ничем, и его нет смысла использовать.

Виды matcher и их использование

Проще всего объяснить матчеры на примерах. Как уже было сказано, все matcher-ы можно разделить на группы в зависимости от их назначения. Начнем с ключевых – наиболее важных.

Ключевые Matcher-ы библиотеки Hamcrest

Прежде всего это логические:

  • allOf() — И
  • anyOf() — ИЛИ
  • not() — НЕ

Допустим, у нас есть строка, и надо убедиться, что выполняется одно из следующих условий:

  • Либо она содержит («Pet»)
  • Либо она заканчивается на «ya»

Выполним проверку:

@Test
public void givenString_whenConditions_thenCorrect() {
    String string = "Petya";
    assertThat(string, anyOf(containsString("Pet"), endsWith("tya")));
}

Допустим, есть число, и надо проверить, что для него выполняются все следующие условия:

  • Число больше 5
  • Число не равно 6
  • Число меньше или равно 7
@Test
public void givenNumber_whenConditions_thenCorrect() {
    Integer intVal = 7;
    assertThat(intVal, allOf(greaterThan(5), lessThanOrEqualTo(7), not(equalTo(6))));
}

Обратите внимание, что использованные выше матчеры greaterThan(), lessThanOrEqualTo(), equalTo() сравнивают объекты, а не числа. Числа тут всего лишь частный случай.

Метод equalTo() доступен для всех объектов, так как задействует метод из Object.

Методы greaterThan(), lessThanOrEqualTo() доступны для всех объектов, реализующих интерфейс Comparable.

Есть еще матчеры greaterThanOrEqualTo(), lessThan(), дополняющие набор матчеров сравнения.

Еще существует декоратор is(), он ничего не добавляет, кроме читабельности. Согласитесь, что:

assertThat(intVal, is(equalTo(1)));

читается легче с частицей is.

Рассмотрим еще парочку ключевых матчеров.

Допустим, есть объект. Надо убедиться, что его значение не равно null:

@Test
public void whenNotNull_thenCorrect() {
    String str = new String();
    assertThat(str, notNullValue());
}

Допустим, есть объект. Надо убедиться, что это тот же самый экземпляр, что и в аргументе матчера:

@Test
public void whenSameObject_thenCorrect() {

    Object object = new String();

    assertThat(object, sameInstance(object));
}

Для чисел есть особые матчеры, прежде всего они касаются сравнения действительных чисел.

Hamcrest Number Matchers

Известно, что сравнивать числа с плавающей запятой в Java можно только с некоторой погрешностью, обычное равенство тут не работает. Для сравнения таких чисел существует матчер closeTo().

Допустим, надо убедиться, что заданное число равно 7.7 с погрешностью 0.005.

@Test
public void givenDouble_whenCompare_thenCorrect() {
    Double doubleVal = 7.70001;
    assertThat(doubleVal, closeTo(7.7, 0.005));
}

Сравнение же на >, >=, <, <= выполняется матчерами greaterThan() и т.п., рассмотренными выше.

Hamcrest String Matchers

Допустим, есть строка. Надо проверить, что она равна «petya», независимо от регистра (большие или маленькие буквы):

@Test
public void given2Strings_whenEqual_thenCorrect() {
    String a = "Petya";
    String b = "petya";
    assertThat(a, equalToIgnoringCase(b));
}

А теперь допустим имеется строка, и надо проверить, что строка равна «Hi Petya»; При этом сравнивать надо без учета лишних пробелов, так что в начале и в конце строки пробелы удаляются, а в середине все подряд идущие пробелы сводятся к одному:

@Test
 public void given2Strings_whenEqualIgnWS_thenCorrect() {

     String a = "  Hi   Petya ";
     assertThat(a, equalToIgnoringWhiteSpace("Hi Petya"));
 }

Hamcrest Collections Matchers

Рассмотрим матчеры для работы с коллекциями и массивами.

Имеется список. Необходимо убедиться, что он не пустой:

@Test
  public void givenList_whenCheck_thenNotEmpty() {
      List<Integer> list = Arrays.asList(5, 2, 4);
      assertThat(list, is(not(empty())));

  }

Имеется список. Необходимо убедиться, что его размер равен 3:

@Test
public void givenList_whenCheck_thenSize3() {
    List<Integer> list = Arrays.asList(5, 2, 4);
    assertThat(list, hasSize(3));

}

Имеется список. Необходимо убедиться, что все его элементы больше 0:

@Test
 public void givenList_whenCheck_thenPosotiveElements() {
     List<Integer> list = Arrays.asList(5, 2, 4);
     assertThat(list, everyItem(greaterThan(0)));

 }

Имеется список. Необходимо убедиться, что он содержит элемент 5:

@Test
public void givenList_whenCheck_thenHasItem() {
    List<Integer> list = Arrays.asList(5, 2, 4);
    assertThat(list, hasItem(5));

}

Имеется список. Надо убедиться, что он состоит из элементов 5, 2, 4:

@Test
public void givenList_whenCheck_thenContainsElements() {
    List<Integer> list = Arrays.asList(5, 2, 4);
    assertThat(list, contains(5, 2, 4));

}

Имеется Map. Надо убедиться, что в нем есть ключ 1, значение a, а также запись (3, «с»):

@Test
 public void givenMap_whenCheck_thenContains() {
     Map<Integer, String> map = new HashMap<Integer, String>();
     map.put(1, "a");
     map.put(2, "b");
     map.put(3, "c");

     assertThat(map, hasKey(1));
     assertThat(map, hasValue("a"));
     assertThat(map, hasEntry(3, "c"));
 }

Теперь перейдем к массивам.

Надо убедиться, что массив не пустой:

@Test
 public void givenArray_whenCheck_thenNotEmpty() {
     String[] array = new String[] { "ab" };
     assertThat(array, not(emptyArray()));

 }

Надо убедиться, что размер массива равен 3:

@Test
public void givenArray_whenCheck_thenSize3() {
    Integer[] array = new Integer[] { 5, 2, 4 };
    assertThat(array, arrayWithSize(3));

}

Убедиться, что массив содержит элементы 2, 4 и 5 в любом порядке:

@Test
 public void givenArray_whenCheck_thenContainInAnyOrder() {
     Integer[] array = new Integer[] { 5, 2, 4 };
     assertThat(array, arrayContainingInAnyOrder(2, 5, 4));

 }

Убедиться, что в массиве есть элемент 5:

@Test
public void givenArray_whenCheck_thenHasItem() {
    Integer[] array = new Integer[] { 5, 2, 4 };
    assertThat(array, hasItemInArray(5));

}

Убедиться, что массив состоит из элементов 5, 2, 4 в заданном порядке:

@Test
 public void givenArray_whenCheck_thenContainsElements() {
     Integer[] array = new Integer[] { 5, 2, 4 };
     assertThat(array, arrayContaining(5, 2, 4));

 }

В вышеприведенном примере для сравнения используется метод класса Object equalTo().

Hamcrest Object Matchers

Существует набор матчеров для тестирования объектов и бинов. Определим пару классов, с которыми мы будем работать, класс Animal и его наследника класс Dog:

public class Animal {
    protected String sound;
  
    public String getSound() {
        return sound;
    }

    public void setSound(String sound) {
        this.sound = sound;
    }
    public Animal(String sound) {
        this.sound = sound;
    }
}

public class Dog extends Animal {

    public Dog(String sound) {
        super(sound);
    }

    public String toString() {
        return this.sound;
    }
}

Допустим, есть объект. В нашем случае экземпляр класса Dog. Проверим, что его метод toString() возвращает заданное значение:

@Test
public void givenDog_whenToString_thenReturnsSound() {

    Dog dog = new Dog("gaf");
    assertThat(dog, hasToString("gaf"));
}

Проверим, что класс Dog явлется подтипом класса Animal. A класс Integer явлется подтипом класса Number:

@Test
 public void givenSubclass_whenTypeCompatable_thenTrue() {

     assertThat(Dog.class, typeCompatibleWith(Animal.class));
     assertThat(Integer.class, typeCompatibleWith(Number.class));

 }

Убедимся, что данный объект является экземпляром класса Animal или его подкласса:

@Test
public void givenSubclass_whenChecked_thenTrue() {

    Dog dog = new Dog("gaf");
    assertThat(dog, instanceOf(Animal.class));
    assertThat(dog, isA(Animal.class));
}

Этот матчер представляет собой аналог оператора instanceof.

Hamcrest Bean Matchers

Есть также матчеры, проверяющие свойства java-бина — как наличие свойства, так и его значение.

Например, убедимся, что бин Animal имеет свойство sound:

@Test
public void givenBean_whenCheckProperty_thenHas() {

    Animal animal = new Animal("gaf");
    Dog dog = new Dog("gaf");

    assertThat(animal, Matchers.<Animal> hasProperty("sound"));
    assertThat(dog, Matchers.<Animal> hasProperty("sound"));
}

Обратите внимание, что это проверка java-бинов, а в бинах должны присутствовать геттеры и сеттеры. Если их нет, проверка не пройдет.

Выше сделана также проверка экземпляра Dog на наличие свойства – проверка проходит, хотя свойство sound у класса Dog унаследовано.

Проверим, что значение свойства sound у бина animal равно gaf:

@Test
public void givenBean_whenCheckPropertyValue_thenEqual() {

    Animal animal = new Animal("gaf");

    assertThat(animal, Matchers.<Animal> hasProperty("sound", equalTo("gaf")));
}

Проверим, что значения свойств у двух объектов совпадают:

@Test
public void given2Beans_whenHavingSameValues_thenCorrect() {
    Animal animal1 = new Animal("gaf");
    Animal animal2 = new Animal("gaf");
    assertThat(animal1, samePropertyValuesAs(animal2));
}

Custom Matcher

Рассмотрим, как написать свой матчер.

Допустим, у нас есть условие, которое надо проверить, но при этом стандартные матчеры не подходят, либо с  их помощью условие записывать слишком сложно. А мы хотим выполнить проверку красиво — с помощью одного матчера.

Здесь-то и пригодится свой собственный пользовательский матчер (custom matcher).

Например, мы хотим проверять единым матчером сложное условие из трех составных частей, которое мы рассмотривали выше:

  • Число больше 5
  • Число не равно 6
  • Число меньше или равно 7

Для написания своего матчера можно расширить класс TypeSafeDiagnosingMatcher<T>, где T —  тип объекта, который надо проверить. В нашем случае это число — тип Integer.

public class OurNumberMatcher extends TypeSafeDiagnosingMatcher<Integer> {

    public void describeTo(Description description) {
          description.appendText("GreaterThan 5 and LessThanOrEqual to 7 and        
          notEqualtTo 6");

    }

    @Override
    protected boolean matchesSafely(Integer item, 
        Description mismatchDescription) {
        int i = item.intValue();
        mismatchDescription.appendText("was ")
            .appendValue(i)
            .appendText(", which is not what we need");
        return (i > 5 && i <= 7 && i != 6);

    }

    public static OurNumberMatcher isOurNumber() {
        return new OurNumberMatcher();
    }

}

Мы написали три метода.

matchesSafely() — метод, в котором выполняется проверка. Метод абстрактный, и мы обязаны его переопределить.
Первый аргумент в нем — это проверяемый объект, над ним и выполняем проверку. Мы возвращаем true, если число соответствует нашим трем условиям.

Второй аргумент — это сообщение, которое выдается в случае, если проверка не проходит.
Например, если вышеприведенном  примере с числами из предыдущего раздела проверить 9 на соответствие условиям, проверка не пройдет. Hamcrest выдает сообщение, состоящее их двух частей. Одна часть идет после слова Expected — какой объект ожидался. А вторая после слова but — что имеем в реальности. Так вот вторая часть сообщения и записывается во второй аргумент (у нас в методе для простоты записано краткое сообщение, а не то, что в стандартных матчерах):

java.lang.AssertionError: 
Expected: (a value greater than <5> and a value less than or equal to <7> and not <6>)
but: a value less than or equal to <7> <9> was greater than <7>
at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:20)

Первая же часть сообщения (после слова Expected) прописывается в методе describeTo() который тоже следует переопределить. В нем в аргумент description надо вставить описание ожидаемого объекта.

И наконец, последнее — для того, чтобы использовать вновь созданный матчер, создадим статический метод isOurNumber(), возвращаещий экземпляр нашего матчера. Это и есть имя предиката, с помощью которого мы будем выполнять пользовательскую проверку:

@Test
public void givenNumber_whenOurConditions_thenCorrect() {
    Integer intVal = 7;
    assertThat(intVal, OurNumberMatcher.isOurNumber());
}

Итог

В этой статье мы рассмотрели матчеры библиотеки Hamcrest по типам и написали наш собственный пользовательский матчер для проверки целого числа.

Все примеры доступны для скачивания на GitHub.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *