Статьи

Выразительные тесты с Hamcrest

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


Каждый помощник Hamcrest помогает вам писать тесты, которые читаются очень естественно.

Выразительность Hamcret возникла с JMock, но только после добавления уникального assertThat() он был преобразован в автономную библиотеку и независимо использовался в assertThat() тестирования.

После первоначального внедрения Java, стали доступны реализации Hamcrest на нескольких языках программирования. JUnit, RSpec и другие инфраструктуры тестирования реализуют собственный синтаксис Hamcrest, устраняя необходимость явного включения любых библиотек. В связи с быстрым внедрением Hamcret платформы тестирования были перегруппированы в следующие категории:

  • Среды тестирования первого поколения были очень простыми, с одним методом assert() с использованием вроде: assert(x==y) . Программистам было сложно писать выразительные и хорошо организованные тесты. Это также требовало знаний в области программирования для понимания более сложных условий и усложняло написание.
  • Среды тестирования второго поколения , такие как PHPUnit, предлагают большой набор различных утверждений. Эти структуры извлекали действие или предикат из параметров (x == y) в имена утверждений, такие как: assertEquals($expected, $actual) . Это сделало тесты более выразительными и упростило определение пользовательских функций утверждения.
  • assertThat() тестирования третьего поколения используют один метод утверждений ( assertThat() ) в сочетании с выразительными сопоставлениями, делая утверждения читаемыми как английские предложения: assertThat($calculatedResult, equalTo($expectedResult)) в отличие от assertEquals($expectedResult, $calculatedResult) .

Использование совпадений Hamcrest также может помочь в других отношениях; Вы можете написать свои собственные сопоставители и использовать их внутри функции assertThat() . Hamcret также предоставляет гораздо больше информации, когда что-то идет не так. Вместо неясного сообщения, такого как «Ожидаемое значение не является истинным», ошибки Hamcrest фактически сообщают все значения, связанные с тестом, то есть как ожидаемые, так и фактические значения. Сопоставители также допускают гибкие утверждения, поэтому тесты не терпят неудачу после внесения небольших изменений, которые не должны нарушать тест. Другими словами, хрупкие тесты являются более надежными.


Есть несколько способов установить Hamcrest. Два наиболее распространенных включают использование PEAR или загрузку исходного кода. На момент написания этой статьи Hamcrest для PHP еще не был доступен через Composer.

Использовать PEAR для установки Hamcrest легко. Просто запустите следующие команды:

1
2
pear channel-discover hamcrest.googlecode.com/svn/pear
pear install hamcrest/Hamcrest

Убедитесь, что у вас есть папка установки PEAR в вашем глобальном пути. Это облегчает включение Hamcrest в ваши тесты.

Вы всегда можете скачать последнюю версию Hamcrest со страницы загрузки проекта и использовать ее как любую стороннюю библиотеку PHP.


Давайте сначала удостоверимся, что у нас есть рабочий скелетный тест с включенным Hamcrest Создайте проект в вашей любимой IDE или редакторе кода и создайте тестовый файл. Я только что создал новый проект в NetBeans с папкой Test в качестве основной папки. Внутри этой папки находится пустой файл с именем HamcrestMatchersForPHPTest.php . Это будет тестовый файл, и его содержимое будет следующим:

1
2
3
4
5
6
7
8
require_once ‘Hamcrest/Hamcrest.php’;
 
class HamcrestMatchersForPHPTest extends PHPUnit_Framework_TestCase {
 
    function testHamcrestWorks() {
        assertThat(‘a’, is(equalTo(‘a’)));
    }
}

Первая строка включает в себя библиотеку Hamcrest. Обратите внимание, что он имеет заглавную букву «H» для имени папки и имени файла. Официальный файл readme Hamcrest содержит опечатку.

Затем наш единственный тест testHamcrestWorks() утверждает, что a равно a . Очевидно, это проходной тест.

Прохождение теста

Этот первый простой пример естественно читать. Утверждают, что «а» равно «а». Это почти не нуждается в объяснении.

Единственный метод утверждения, который мы будем использовать в наших тестах, это assertThat() . Соответствие is() является просто синтаксическим сахаром; это ничего не делает, кроме как построить ваше предложение. Наконец, equalTo() сравнения equalTo() сравнивает первый параметр с assertThat() со значением, equalTo() . Этот тест фактически переводится как 'a' == 'a' .


Давайте начнем с простого примера:

01
02
03
04
05
06
07
08
09
10
11
12
13
function testNumbers() {
    assertThat(2, equalTo(2));
 
    assertThat(2, lessThan(3));
    assertThat(3, greaterThan(2));
 
    assertThat(2, is(identicalTo(2)));
    assertThat(2, comparesEqualTo(2));
 
    assertThat(2, is(closeTo(3, 1)));
 
    assertThat(2, is(not(3)));
}

Вы можете написать свои собственные сопоставители и использовать их внутри функции assertThat() .

Этот код вводит новые соответствия; первые два — lessThan() и greaterThan() . Эти сопоставители эквивалентны операторам сравнения «<» и «>».

Функция identicalTo() предоставляет еще один способ проверки равенства. Фактически, это эквивалент оператора === , тогда как equalTo() == . Еще одно новое средство сравнения в этом коде — comparesEqualTo() , эффективно выполняющее эквивалент !($x < $y || $x > $y) . В приведенном выше коде значения $x и $y равны 2 (поэтому тест пройден).

Одним из наиболее интересных совпадений является closeTo() . Он принимает два аргумента: целевое значение и допустимая разница соответственно. В этом примере проверяется, близко ли 2 к 3 максимум 1 . Это, очевидно, правильно, и утверждение проходит.

Наконец, последнее утверждение — это просто простое отрицание в сочетании с is() . Это, очевидно, утверждает, что 2 не 3 .

Hamcrest также предоставляет средства сравнения, которые приравниваются к операторам сравнения <= и >= . Они точно названы lessThanOrEqualTo() и greaterThanOrEqualTo() , и они используются так:

01
02
03
04
05
06
07
08
09
10
11
12
13
function testNumbersComposed() {
    assertThat(2, lessThanOrEqualTo(2));
    assertThat(2, lessThanOrEqualTo(3));
 
    assertThat(3, greaterThanOrEqualTo(3));
    assertThat(3, greaterThanOrEqualTo(2));
 
    assertThat(2, is(atMost(2)));
    assertThat(2, is(atMost(3)));
 
    assertThat(3, is(atLeast(3)));
    assertThat(3, is(atLeast(2)));
}

Hamcrest также предоставляет atMost() и atLeast() . lessThanOrEqualTo() и atMost() идентичны. Они оба равны $x <= $y . Естественно, greaterThanOrEqualTo() и atLeast() выполняют прямо противоположное, проверяя $x >= $y .


Hamcrest также предоставляет несколько устройств для работы со строками. Вот некоторые примеры:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
function testStrings() {
    assertThat(‘this string’, equalTo(‘this string’));
    assertThat(‘this string’, equalToIgnoringCase(‘ThiS StrIng’));
    assertThat(‘this string’, equalToIgnoringWhiteSpace(‘ this string ‘));
    //assertThat(‘this string’, equalToIgnoringWhiteSpace(‘thisstring’));
    assertThat(‘this string’, identicalTo(‘this string’));
 
    assertThat(‘this string’, startsWith(‘th’));
    assertThat(‘this string’, endsWith(‘ing’));
 
    assertThat(‘this string’, containsString(‘str’));
    assertThat(‘this string’, containsStringIgnoringCase(‘StR’));
 
    assertThat(‘this string’, matchesPattern(‘/^this\s*/’));
}

Я рекомендую вам использовать более выразительные совпадения, когда это возможно …

Очевидно, что equalTo() и identifTo identicalTo() работают со строками и ведут себя точно так, как вы ожидаете. Но, как вы можете видеть, Hamcrest предоставляет другие специфичные для строки соответствия. Как equalToIgnoringCase() их названий, функции equalToIgnoringCase() и equalToIgnoringWhiteSpace() сопоставляют строки, игнорируя регистр и пробел, соответственно.

Другие сопоставители, такие как startsWith() и endsWith() , проверяют, находится ли указанная подстрока в начале или конце фактической строки. Содержимое containsString() проверяет, содержит ли строка предоставленную подстроку. Сочетание containsString() также может быть расширено с помощью containsStringIgnoringCase() , добавляя нечувствительность к регистру.

Функция matchesPattern() включает регулярные выражения для поиска соответствия в строке. Вы можете использовать любое регулярное выражение, и во многих случаях это решение необходимо в более сложных строках. В любом случае, я рекомендую вам использовать более выразительные сопоставления всякий раз, когда это возможно, и прибегать к регулярным выражениям только в случае крайней необходимости; это делает ваш код более читабельным для всех.

Обычно проверяют, является ли строка пустой. Хамкрест тебя прикрыл.

01
02
03
04
05
06
07
08
09
10
11
function testStringEmptiness() {
    assertThat(», isEmptyString());
    assertThat(», emptyString());
    assertThat(», isEmptyOrNullString());
    assertThat(NULL, isEmptyOrNullString());
    assertThat(», nullOrEmptyString());
    assertThat(NULL, nullOrEmptyString());
 
    assertThat(‘this string’, isNonEmptyString());
    assertThat(‘this string’, nonEmptyString());
}

Да, есть много соответствий для проверки, является ли строка пустой. У каждого варианта есть версия с «есть» перед ним. По сути, emptyString() и isEmptyString() идентичны, и то же самое верно для других сопоставителей. nonEmptyString() и isNonEmptyString() также можно записать так:

1
2
3
assertThat(‘this string’, not(isEmptyString()));
assertThat(‘this string’, is(not(emptyString())));
assertThat(‘this string’, is(nonEmptyString()));

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


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

1
2
3
4
5
6
7
8
9
function testInclusionsExclusions() {
    assertThat(‘val’, is(anyOf(‘some’, ‘list’, ‘of’, ‘val’)));
    assertThat(‘val’, is(noneOf(‘without’, ‘the’, ‘actual’, ‘value’)));
 
    assertThat(‘this string’, both(containsString(‘this’))->andAlso(containsString(‘string’)));
    assertThat(‘this string’, either(containsString(‘this’))->orElse(containsString(‘that’)));
 
    assertThat(‘any value, string or object’, is(anything()));
}

В этих примерах используются строки, но вы можете использовать эти сопоставители с такими переменными, как объекты, массивы, числа и т. Д. anyOf() и noneOf() определяют, находится ли ожидаемая переменная в предоставленном списке значений.

Два других сопоставителя, both() и either() , обычно используются с andAlso() и orElse() . Это эквиваленты:

1
2
assertThat((strpos(‘this string’, ‘this’) !== FALSE) && (strpos(‘this string’, ‘string’) !== FALSE));
assertThat((strpos(‘this string’, ‘this’) !== FALSE) || (strpos(‘this string’, ‘string’) !== FALSE));

Наконец, anything() соответствует … ну, что угодно. Он имеет необязательный строковый параметр для целей метаданных, помогая любому, читающему тест, лучше понять, почему утверждение всегда должно совпадать.


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

Обратите внимание: я не делаю разницы между массивами и хешами в этих примерах. Я говорю только о массивах, но все относится и к хэшам.

1
2
3
4
5
6
7
function testArrayEquality() {
    $actualArray = array(1,2,3);
 
    $expectedArray = $actualArray;
    assertThat($actualArray, is(anArray($expectedArray)));
    assertThat($actualArray, equalTo($expectedArray));
}

Эти методы выделяют разные способы сравнения равенства двух массивов. Первая версия использует вводящее в заблуждение имя anArray() matcher. На самом деле он сравнивает два массива элемент за элементом. При сбое в сообщении об ошибке отображается только первый набор неравных элементов.

Вторая версия, использующая equalTo() , также сравнивает каждый элемент в массивах, но выводит оба массива целиком в случае сбоя. Естественно, длина ваших массивов будет определять, какой сопоставитель вы используете. Чтение больших массивов может быть затруднено.

Во многих случаях мы просто хотим проверить, содержит ли массив определенные элементы. Хамкрест нас прикрыл.

01
02
03
04
05
06
07
08
09
10
11
12
function testArrayPartials() {
    $actualArray = array(1,2,3);
 
    assertThat($actualArray, hasItemInArray(2));
    assertThat($actualArray, hasValue(2));
 
    assertThat($actualArray, arrayContaining(atLeast(0),2,3));
    assertThat($actualArray, contains(1,2,lessThan(4)));
 
    assertThat($actualArray, arrayContainingInAnyOrder(2,3,1));
    assertThat($actualArray, containsInAnyOrder(3,1,2));
}

hasItemInArray() и hasValue() идентичны; они оба проверяют, существует ли предоставленное значение или результат сопоставления в массиве. Предоставление значения в качестве аргумента эквивалентно использованию equalTo() . Следовательно, они идентичны: hasValue(2) и hasValue(equalTo(2)) .

Следующие два сопоставителя, arrayContaining() и contains() , также идентичны. Они по порядку проверяют, что каждый элемент массива удовлетворяет соответствию или что каждый элемент равен указанным значениям.

Наконец, как вы можете легко вывести из приведенного выше примера, arrayContainingInAnyOrder() и containsInAnyOrder() совпадают с двумя предыдущими сопоставлениями. Разница лишь в том, что они не заботятся о порядке .

01
02
03
04
05
06
07
08
09
10
11
function testArrayKeys() {
    $actualArray[‘one’] = 1;
    $actualArray[‘two’] = 2;
    $actualArray[‘three’] = 3;
 
    assertThat($actualArray, hasKeyInArray(‘two’));
    assertThat($actualArray, hasKey(‘two’));
 
    assertThat($actualArray, hasKeyValuePair(‘three’, 3));
    assertThat($actualArray, hasEntry(‘one’, 1));
}

Соответствующие функции hasKeyInArray() и hasKey() проверяют, соответствует ли данный аргумент каким-либо ключам в массиве, в то время как последние два соответствия возвращают значение true, только если найдены оба ключа и значение. Итак, сопоставитель типа hasEntry('one', 2); провалил бы наш тест, потому что в нашем массиве по ключу ‘ one ‘ мы имеем значение 1 а не 2

Обратите внимание: рекомендуется использовать более короткую версию ( hasKey ), когда из имени переменной очевидно, что это массив. Любой, кто читает ваш код, может быть смущен типом переменной, и в этом случае ( hasKeyInArray ) может быть более полезным.

Вы можете легко проверить размер массива с помощью трех соответствий:

1
2
3
4
5
6
7
function testArraySizes() {
    $actualArray = array(1,2,3);
 
    assertThat($actualArray, arrayWithSize(3));
    assertThat($actualArray, nonEmptyArray());
    assertThat(array(), emptyArray());
}

arrayWithSize() проверяет наличие определенного размера, nonEmtpyArray() проверяет, nonEmtpyArray() ли массив хотя бы один элемент, а emptyArray() проверяет, является ли данный массив пустым.

Обратите внимание: существуют версии этих трех сопоставителей, если вы предпочитаете итерируемые объекты. Просто замените «Array» на «Traversable», как этот traversableWithSize() .


Это последние матчеры. Вы можете практически проверить любой тип данных в PHP.

01
02
03
04
05
06
07
08
09
10
11
12
13
function testTypeChecks() {
    assertThat(NULL, is(nullValue()));
    assertThat(», notNullValue());
    assertThat(TRUE, is(booleanValue()));
    assertThat(123.45, is(numericValue()));
    assertThat(123, is(integerValue()));
    assertThat(123.45, is(floatValue()));
    assertThat(‘aString’, stringValue());
    assertThat(array(1,2,3), arrayValue());
    assertThat(new MyClass, objectValue());
    assertThat(new MyClass, is(anInstanceOf(‘MyClass’)));
    // there are a few other more exotic value checkers you can discover on your own
}

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


Hamcrest PHP не достаточно подробно документирован в официальной документации, и я надеюсь, что это руководство помогло объяснить, какие средства он предоставляет. В этом уроке не указано несколько совпадений, но они очень экзотичны и редко используются.

Каждый помощник Hamcrest помогает вам писать тесты, которые читаются очень естественно.

Если их вам недостаточно, вы можете добавить свои собственные соответствия для любой ситуации, в которой они вам нужны. Я надеюсь, вам понравился этот урок, и спасибо за чтение.