Утверждения, написание настоящего теста и аннотация @dataProvider

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

Но этот тест познакомил вас с самым базовым утверждением PHPUnit — assertTrue().

УТВЕРЖДЕНИЯ

Что такое утверждение?

Википедия дает следующее определение утверждения:

предикат (выражение истина-ложь), помещенный в программу и обозначающий, что разработчик считает этот предикт в данном месте всегда истинным.

Иными словами, утверждение подтверждает, что выражение равняется true

Утверждения равняются true

В нашем первом примере

public function testTrueIsTrue()
{
    $foo = true;
    $this->assertTrue($foo);
}

мы утверждали, что true будет равняться true ( если (true == true) ). Здесь нет никакого волшебства: когда дело касается утверждений, то вы получаете ровно то, что видите.

Если мы утверждаем, что false равняется true ( если (false == true) ), мы получим непроходимый тест:

public function testTrueIsTrue()
{
    $foo = false;
    $this->assertTrue($foo);
}

А что если бы мы утверждали, что false равняется false ( если (false == false) )?

public function testFalseIsFalse()
{
    $foo = false;
    $this->assertFalse($foo);
}

Тест был бы пройден, потому что наше утверждение было бы true, даже если метод утверждения называется assertFalse().

Доступные утверждения

PHPUnit работает с 90 утверждениями, список которых приведен здесь. Если вы используете соответствующую интегрированную среду разработки, вам не нужно будет их запоминать, поскольку они доступны через $this->assert*. Более того, вам не надо использовать все эти утвеждения. Вы будете преимущественно использовать assertArrayHasKey(), assertEquals(), assertFalse(), assertSame()и assertTrue(). За время тестирования своего кода я использовал не более 15% всех доступных утверждений, поэтому в дальнейшем я заострю внимание на этих пяти методах утверждения, и, возможно, слегка коснусь пары других.

Индивидуальные утверждения

В PHPUnit методы утверждений это стандартные методы, которые после оценки кода показывают либо true, либо false. Если вы не можете найти утверждение, которые бы в точности удовлетворяло вашему требованию, вы можете создать новое утвреждение – это так же просто, как и создать новый метод! Для этого не нужно изучать сложную модульную архитектуру. Просто задайте метод в своем классе теста и начните его использовать. Если он понадобится вам в более чем одном классе, переместите его в родительский класс, который расширяется за счет всех ваших тестов.

В будущем я остановлюсь на этом вопросе более подробно.

Первый настоящий тест

Итак, довольно введения в API! Если хотите, вы можете ознакомиться с официальным руководством по PHPUnit.

Предупреждение: текст изложения там очень сухой.

Код

В качестве нашего первого нестандартного теста мы создадим тест для метода преобразования в слаг (slug). Этот метод превратит обычную строку в совместимую с URL строку: «Эта строка будет преобразована в слаг» превратится в эта-строка-будет-преобразована-в-слаг.

Создайте новый файл в ./phpUnitTutorial/URL.php  и вставьте следующее:

<?php
namespace phpUnitTutorial;
class URL
{
    public function sluggify($string, $separator = '-', $maxLength = 96)
    {
        $title = iconv('UTF-8', 'ASCII//TRANSLIT', $string);
        $title = preg_replace("%[^-/+|\w ]%", '', $title);
        $title = strtolower(trim(substr($title, 0, $maxLength), '-'));
        $title = preg_replace("/[\/_|+ -]+/", $separator, $title);
        return $title;
    }
}

Это отличный пример для первого настоящего теста, поскольку код прост для понимания, но при этом в нем можно легко допустить ошибки.

Что мы ожидаем

Как уже было сказано, мы хотим применить этот метод к любой строке и ожидаем получить интерпретируемый, подходящий для URL слаг.

Тест

Начните с создания файла теста в ./phpUnitTutorial/Test/URLTest.php и вставьте основу:

<?php
namespace phpUnitTutorial\Test;
class URLTest extends \PHPUnit_Framework_TestCase
{
    //
}

Неудача!

Если вы прямо сейчас запустите PHPUnit, то увидите следующее:

FAILURES!
Tests: 2, Assertions: 1, Failures: 1.

Вы видите 2 теста, 1 утвреждение и одно сообщение об ошибке. Второй тест взят из первой части этой серии статей.

Выполняемый тест не был пройден, потому что в данный момент в ./phpUnitTutorial/Test/URLTest.php не содержится никаких тестов.

Это вполне нормально.

Мы выполним наш комплект тестов сразу после создания файла и основы, чтобы проверить, что мы не допустили ошибку с файлом или именем класса теста. В будущем это поможет избежать проблем в тех случаях, когда ваш тест показывает зеленую строку, но потом выясняется, что PHPUnit не выполнял файл, потому что вы наверняка ошиблись с одним из соглашений о присвоении имен. Теперь задача состоит в том, чтобы из красной строки сделать зеленую. Единственный способ это сделать – создать пустой метод теста.

В случае с нашим первым тестом мы хотим проверить, что phpUnitTutorial\URL::sluggify() возвращает преобразованную в слаг строку, поэтому назовите свой метод теста соответствующим образом:

<?php
namespace phpUnitTutorial\Test;
class URLTest extends \PHPUnit_Framework_TestCase
{
    public function testSluggifyReturnsSluggifiedString()
    {
        //
    }
}

Успех!

PHPUnit теперь показывает долгожданную зеленую строку!

Теперь мы можем перейти к созданию содержимого нашего теста.

Вкусные внутренности

Мы должны начать тест с наших ожиданий:

«This string will be sluggified» («Эта строка будет преобразована в слаг») будет преобразовано в «this-string-will-be-sluggified» («эта-строка-будет-преобразована-в-слаг»).

<?php
namespace phpUnitTutorial\Test;
class URLTest extends \PHPUnit_Framework_TestCase
{
    public function testSluggifyReturnsSluggifiedString()
    {
        $originalString = 'This string will be sluggified';
        $expectedResult = 'this-string-will-be-sluggified';
    }
}

Чтобы протестировать phpUnitTutorial\URL::sluggify(), нам нужно инстанцировать объект класс URL. Это очень просто:

<?php
namespace phpUnitTutorial\Test;
use phpUnitTutorial\URL;
class URLTest extends \PHPUnit_Framework_TestCase
{
    public function testSluggifyReturnsSluggifiedString()
    {
        $originalString = 'This string will be sluggified';
        $expectedResult = 'this-string-will-be-sluggified';
        $url = new URL();
    }
}

Ради удобства я также добавил оператор use, чтобы при инстанцировании объекта нам не пришлось использовать все пространство имен.

Теперь возьмите результат из метода ::sluggify():

<?php
namespace phpUnitTutorial\Test;
use phpUnitTutorial\URL;
class URLTest extends \PHPUnit_Framework_TestCase
{
    public function testSluggifyReturnsSluggifiedString()
    {
        $originalString = 'This string will be sluggified';
        $expectedResult = 'this-string-will-be-sluggified';
        $url = new URL();
        $result = $url->sluggify($originalString);
    }
}

Завершающий шаг это утверждение того, что $result равняется нашим ожиданиям, обозначенным $expectedResult. Идеальное утверждение —  assertEquals():

<?php
namespace phpUnitTutorial\Test;
use phpUnitTutorial\URL;
class URLTest extends \PHPUnit_Framework_TestCase
{
    public function testSluggifyReturnsSluggifiedString()
    {
        $originalString = 'This string will be sluggified';
        $expectedResult = 'this-string-will-be-sluggified';
        $url = new URL();
        $result = $url->sluggify($originalString);
        $this->assertEquals($expectedResult, $result);
    }
}

Запускаем PHPUnit — и видим отличный результат:

Другие сценарии

Наш первый тест пройден, и это отлично! Но есть небольшая проблемка: мы протестировали только то, что строка, содержащая буквы A-Z и пробелы, возвращает ожидаемый результат. А что было бы, если бы мы протестировали строку с числами? Со специальными символами (~!@#$%^&*()_+)? А что насчет не-английских символов? А что если мы протестируем пустую строку?! Существует столько вариантов, а наш тест покрывает лишь малую их часть.

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

<?php
namespace phpUnitTutorial\Test;
use phpUnitTutorial\URL;
class URLTest extends \PHPUnit_Framework_TestCase
{
    public function testSluggifyReturnsSluggifiedString()
    {
        $originalString = 'This string will be sluggified';
        $expectedResult = 'this-string-will-be-sluggified';
        $url = new URL();
        $result = $url->sluggify($originalString);
        $this->assertEquals($expectedResult, $result);
    }
    public function testSluggifyReturnsExpectedForStringsContainingNumbers()
    {
        $originalString = 'This1 string2 will3 be 44 sluggified10';
        $expectedResult = 'this1-string2-will3-be-44-sluggified10';
        $url = new URL();
        $result = $url->sluggify($originalString);
        $this->assertEquals($expectedResult, $result);
    }
    public function testSluggifyReturnsExpectedForStringsContainingSpecialCharacters()
    {
        $originalString = 'This! @string#$ %$will ()be "sluggified';
        $expectedResult = 'this-string-will-be-sluggified';
        $url = new URL();
        $result = $url->sluggify($originalString);
        $this->assertEquals($expectedResult, $result);
    }
    public function testSluggifyReturnsExpectedForStringsContainingNonEnglishCharacters()
    {
        $originalString = "Tänk efter nu – förr'n vi föser dig bort";
        $expectedResult = 'tank-efter-nu-forrn-vi-foser-dig-bort';
        $url = new URL();
        $result = $url->sluggify($originalString);
        $this->assertEquals($expectedResult, $result);
    }
    public function testSluggifyReturnsExpectedForEmptyStrings()
    {
        $originalString = '';
        $expectedResult = '';
        $url = new URL();
        $result = $url->sluggify($originalString);
        $this->assertEquals($expectedResult, $result);
    }
}

Наш комплект тестов пройден!

Копирование кода

Вы, несомненно, являетесь профи в этой области и можете сразу обнаружить ошибку в приведенных выше тестах: они совершенно не соответствуют принципу DRY (не повторяйся). К счастью, в PHPUnit есть встроенный инструмент в виде аннотации dataProvider.

Введение в аннотации

Аннотации это ничто не иное, как специальные флаги, определенные в докблоках вашего метода:

/**
 * @annotationName Annotation value
 */
public function testFoo()
{
    //
}

PHPUnit содержит множество полезных аннотаций, но сейчас нам нужна самая эффективная из них —dataProvider.

@dataProvider

PHPUnit приведено следующее определение поставщика данных :

Метод теста может принимать произвольные аргументы. Эти аргументы должны поставляться методом поставщика данных.

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

Вместо создания множества методов теста, вы создаете один метод, которые принимает параметры, соответствующие данным, изменяющимся от теста к тесту, и создаете метод поставщика данных для предоставления этих данных:

/**
 * @dataProvider providerTestFoo
 */
public function testFoo($variableOne, $variableTwo)
{
    //
}
public function providerTestFoo()
{
    return [
        array('test 1, variable one', 'test 1, variable two'),
        array('test 2, variable one', 'test 2, variable two'),
        array('test 3, variable one', 'test 3, variable two'),
        array('test 4, variable one', 'test 4, variable two'),
        array('test 5, variable one', 'test 5, variable two'),
    ];
}

Итак, мы создали один тест и поставщика данных. Аннотация @dataProviderannotation определяет, какой метод предоставляет данные для теста testFoo().

Все дальше и дальше!

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

Сделайте глубокий вдох, у вас все получится.

Массив первого уровня это просто вместилище для определенного количества наборов данных:

return [
    array('test 1, variable one', 'test 1, variable two'),
    array('test 2, variable one', 'test 2, variable two'),
    array('test 3, variable one', 'test 3, variable two'),
    array('test 4, variable one', 'test 4, variable two'),
    array('test 5, variable one', 'test 5, variable two'),
];

Это не так важно, это лишь означает, что все поставщики данных должны возвращать массив, содержащий другие массивы.

Массивы второго уровня предоставляют фактические наборы данных. В нашем примере имеется 5 массивов второго уровня, что соответствует 5 тестам.

return [
    array('test 1, variable one', 'test 1, variable two'),
    array('test 2, variable one', 'test 2, variable two'),
    array('test 3, variable one', 'test 3, variable two'),
    array('test 4, variable one', 'test 4, variable two'),
    array('test 5, variable one', 'test 5, variable two'),
];

Значения в рамках этих массивов второго уровня это проходимые параметры метода теста. В нашем примере первый набор данных array('test 1, variable one', 'test 1, variable two'), соответствует двум ожидаемым параметрам метода в testFoo():

testFoo($variableOne, $variableTwo)

Это повторяется столько раз, сколько массивов второго уровня содержится в поставщике данных.. Итак, теперь мы применим этот полезный инструмент в нашем тесте!

Тестирование с @dataProvider

Замените содержимое  ./phpUnitTutorial/Test/URLTest.php на:

<?php
namespace phpUnitTutorial\Test;
use phpUnitTutorial\URL;
class URLTest extends \PHPUnit_Framework_TestCase
{
    /**
     * @param string $originalString String to be sluggified
     * @param string $expectedResult What we expect our slug result to be
     *
     * @dataProvider providerTestSluggifyReturnsSluggifiedString
     */
    public function testSluggifyReturnsSluggifiedString($originalString, $expectedResult)
    {
        $url = new URL();
        $result = $url->sluggify($originalString);
        $this->assertEquals($expectedResult, $result);
    }
    public function providerTestSluggifyReturnsSluggifiedString()
    {
        return array(
            array('This string will be sluggified', 'this-string-will-be-sluggified'),
            array('THIS STRING WILL BE SLUGGIFIED', 'this-string-will-be-sluggified'),
            array('This1 string2 will3 be 44 sluggified10', 'this1-string2-will3-be-44-sluggified10'),
            array('This! @string#$ %$will ()be "sluggified', 'this-string-will-be-sluggified'),
            array("Tänk efter nu – förr'n vi föser dig bort", 'tank-efter-nu-forrn-vi-foser-dig-bort'),
            array('', ''),
        );
    }
}

Теперь выполните комплект тестов:

Урррра!

Заключение

Сегодня вы узнали об утверждениях, создали свой перый «настоящий» тест и познакомились с эффективной аннотацией @dataProvider.

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

Но не будем забегать вперед. В следующей части я расскажу, как тестировать код с внешними зависимостями и что это значит; также вы узнаете, что такое мок-объект (mock) и заглушка (stub) и чем они отличаются, чем плохи статические методы и чем полезно внедрение зависимости.