PHPUnit. Часть 03 Написание тестов для PHPUnit

http://www.phpunit.de/manual/3.4/en/writing-tests-for-phpunit.html
  • Перевод
Предисловие переводчика
Эта статья продолжает серию переводов официальной документации по PHPUnit на русский язык.
Часть 1, Часть 2

Пример 4.1 демонстрирует как с помощью PHPUnit можно выполнить тестирование операций с массивами PHP. В этом примере показаны базовые соглашения и шаги, свойственные тестам PHPUnit:
  • Наименование тестирующего класса образуется путем добавления постфикса Test к наименованию тестируемого класса. Например тестируемый класс называется Class, тестирующий — ClassTest.
  • ClassTest наследуется (в большинстве случаев) от PHPUnit_Framework_TestCase.
  • Наименования тестирующих методов образуются путем добавления приставки test к наименованиям тестируемых методов.
  • Внутри тестовых методов утверждения задаются специальными функциями assertEquals()
    (смотрите раздел PHPUnit_Framework_Assert). assertEquals() задает соответствие реально полученного значения и ожидаемого.
Пример 4.1: Тестирование операций с массивами PHP при с использованием PHPUnit
<?php<br>
require_once 'PHPUnit/Framework.php';<br>
<br>
class StackTest extends PHPUnit_Framework_TestCase<br>
{<br>
    public function testPushAndPop()<br>
    {<br>
        $stack = array();<br>
        $this->assertEquals(0, count($stack));<br>
<br>        array_push($stack, 'foo');<br>
        $this->assertEquals('foo', $stack[count($stack)-1]);<br>
        $this->assertEquals(1, count($stack));<br>
<br>        $this->assertEquals('foo', array_pop($stack));<br>
        $this->assertEquals(0, count($stack));<br>
    }<br>
}<br>
?>
<br>
<br>
* This source code was highlighted with Source Code Highlighter.


«Пишите тест в независимости от того хотите ли Вы вывести значение через функцию print или
выражение отладки.»
Martin Fowler

Зависимости тестов


«Unit-тесты обычно пишутся, чтобы помочь разработчику найти и исправить ошибки, выполнить рефакторинг кода и облегчить документирование модулей.
Чтобы достичь эти цели, тесты в идеале должны покрывать все возможные пути в программе.
Обычно один тест покрывает один специфический путь в одной функции или методе.
Однако тестовый метод не обязательно может быть инкапсулированной, независимой сущностью.
Часто существуют не очевидные зависимости между тестами, скрытые в реализации тестовых сценариев».
Adrian Kuhn et. al.
PHPUnit поддерживает декларирование явных зависимостей между тестами.
Такие зависимости не определяют в какой последовательности тесты должны выполняться,
однако они позволяют возвращать экземпляр тестового окружения (fixture) и передавать его в другой тест; источник (producer) передает приемнику(consumer).
  • Источник — это тестовый метод, который возвращает значения, от которых зависят другие методы модуля.
  • Приемник — это тестовый метод, который зависит от одного или более источников и их возвращаемых значений.
Пример 4.2 показывает как использовать аннотацию @depends для описания зависимостей между тестами.

Пример 4.2: Использование аннотации @depends для описания зависимостей
<?php<br>
class StackTest extends PHPUnit_Framework_TestCase<br>
{<br>
    public function testEmpty()<br>
    {<br>
        $stack = array();<br>
        $this->assertTrue(empty($stack));<br>
<br>
        return $stack;<br>
    }<br>
<br>
    /**<br>
     * @depends testEmpty<br>
     */<br>
    public function testPush(array $stack)<br>
    {<br>
        array_push($stack, 'foo');<br>
        $this->assertEquals('foo', $stack[count($stack)-1]);<br>
        $this->assertFalse(empty($stack));<br>
<br>
        return $stack;<br>
    }<br>
<br>
    /**<br>
     * @depends testPush<br>
     */<br>
    public function testPop(array $stack)<br>
    {<br>
        $this->assertEquals('foo', array_pop($stack));<br>
        $this->assertTrue(empty($stack));<br>
    }<br>
}<br>
?>
<br>
<br>
* This source code was highlighted with Source Code Highlighter.

В этом примере первый тест, testEmpty(), создает пустой массив и задает утверждение, что массив пустой. После этого тест возвращает окружение (fixture) в качестве результата.
Второй тест, testPush(), зависит от testEmpty() и получает результат работы testEmpty() в качестве аргумента. И наконец, testPop() зависит от testPush().
Мы хотим, чтобы наше внимание фокусировалось на важных сообщениях о провале тестов, это позволит максимально быстро находить дефекты.
По этой причине PHPUnit пропускает выполнение зависимых тестов, если Источник (основной тест) закончил работу с ошибкой. Улучшение обнаружения дефектов достигается за счет использования зависимостей между тестами, как показано в Примере 4.3.

Пример 4.3: Использование зависимостей между тестами
<?php<br>
class DependencyFailureTest extends PHPUnit_Framework_TestCase<br>
{<br>
    public function testOne()<br>
    {<br>
        $this->assertTrue(FALSE);<br>
    }<br>
<br>
    /**<br>
     * @depends testOne<br>     */<br>
    public function testTwo()<br>    {<br>
    }<br>}<br>?>
<br>
<br>
* This source code was highlighted with Source Code Highlighter.
phpunit --verbose DependencyFailureTest
PHPUnit 3.4.2 by Sebastian Bergmann.

DependencyFailureTest
FS

Time: 0 seconds

There was 1 failure:

1) testOne(DependencyFailureTest)
Failed asserting that <boolean:false> is true.
/home/sb/DependencyFailureTest.php:6

There was 1 skipped test:

1) testTwo(DependencyFailureTest)
This test depends on "DependencyFailureTest::testOne" to pass.

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

У теста может быть несколько аннотаций @depends.
PHPUnit не изменяет последовательность, в которой запускаются тесты,
Вы должны быть уверены, что зависимости будут выполнены до того как тест запустится.

Источники данных (Data Providers)


Тестовый метод может работать с произвольными аргументами.
Аргументы передаются с помощью метода источника данных (provider(), см. Пример 4.4).
Метод источника данных должен быть определен с помощью аннотации @dataProvider.
Метод источника данных должен быть public и должен возвращать массив массивов или объект, поддерживающий интерфейс Iterator, который на каждой итерации возвращает массив.
Для каждого массива, который является частью коллекции, будет вызван тестовый метод. В качестве аргумента методу будет передан массив значений.

Пример 4.4: Использование источника данных
<?php<br>
class DataTest extends PHPUnit_Framework_TestCase<br>
{<br>
    /**<br>
     * @dataProvider provider<br>
     */<br>
    public function testAdd($a, $b, $c)<br>
    {<br>
        $this->assertEquals($c, $a + $b);<br>
    }<br>
<br>
    public function provider()<br>
    {<br>
        return array(<br>
         array(0, 0, 0),<br>
         array(0, 1, 1),<br>
         array(1, 0, 1),<br>
         array(1, 1, 3)<br>
        );<br>
    }<br>
}<br>
?>
<br>
<br>
* This source code was highlighted with Source Code Highlighter.

phpunit DataTest
PHPUnit 3.4.2 by Sebastian Bergmann.

...F

Time: 0 seconds

There was 1 failure:

1) testAdd(DataTest) with data (1, 1, 3)
Failed asserting that <integer:2> matches expected value <integer:3>.
/home/sb/DataTest.php:21

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

Примечание


Если тестовому методу одновременно передаются параметры от источника данных (@dataProvider) и одного или нескольких тестов, которые определены как зависимые (@depends), то в первую очередь используется источник данных и только потом другие тесты.

Тестирование исключений


Пример 4.5 демонстрирует как использовать аннотацию @expectedException для тестирования
исключений, которые выбрасываются внутри тестового кода.

Пример 4.5: Использование аннотации @expectedException
<?php<br>
require_once 'PHPUnit/Framework.php';<br>
<br>
class ExceptionTest extends PHPUnit_Framework_TestCase<br>
{<br>
    /**<br>
     * @expectedException InvalidArgumentException<br>
     */<br>
    public function testException()<br>
    {<br>
    }<br>
}<br>
?>
<br>
<br>
* This source code was highlighted with Source Code Highlighter.

phpunit ExceptionTest
PHPUnit 3.4.2 by Sebastian Bergmann.

F

Time: 0 seconds

There was 1 failure:

1) testException(ExceptionTest)
Expected exception InvalidArgumentException

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

Метод setExpectedException() — это еще один способ указать, что в тестовом методе выбрасывается исключение, см. Пример 4.6.

Пример 4.6: Ожидается, что тестовый метод выбросит исключение
<?php<br>
require_once 'PHPUnit/Framework.php';<br>
<br>
class ExceptionTest extends PHPUnit_Framework_TestCase<br>
{<br>
    public function testException()<br>
    {<br>
        $this->setExpectedException('InvalidArgumentException');<br>
    }<br>
}<br>
?>
<br>
<br>
* This source code was highlighted with Source Code Highlighter.

phpunit ExceptionTest
PHPUnit 3.4.2 by Sebastian Bergmann.

F

Time: 0 seconds

There was 1 failure:

1) testException(ExceptionTest)
Expected exception InvalidArgumentException

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

В таблице 4.1 приведены методы для тестирования исключений.

Таблица 4.1 (текстовый вариант). Методы для тестирования исключений
void setExpectedException(string $exceptionName)
— Установка имени ожидаемого исключения — $exceptionName.
String getExpectedException()
— Получение имени ожидаемого исключения.

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

Пример 4.7: Альтернативный подход к тестированию исключений
<?php<br>
require_once 'PHPUnit/Framework.php';<br>
<br>
class ExceptionTest extends PHPUnit_Framework_TestCase {<br>
    public function testException() {<br>
        try {<br>
            // ... Code that is expected to raise an exception ...<br>
        }<br>
<br>
        catch (InvalidArgumentException $expected) {<br>
            return;<br>
        }<br>
<br>
        $this->fail('An expected exception has not been raised.');<br>
    }<br>
}<br>
?>
<br>
<br>
* This source code was highlighted with Source Code Highlighter.



Ожидается, что код, показанный в Примере 4.7 выбросит исключение. Если это не произойдет, то будет вызван метод fail(), (см.
(см. Таблицу 22.2), который прервет выполнение теста и просигнализирует об ошибке.
Если исключение будет сгенерированно, то сработает блок catch и тест завершится успешно.

Тестирование ошибок PHP


По умолчанию, ошибки, предупреждения и уведомления PHP, которые появляются во время тестирования, PHPUnit преобразует в исключения. Используя эту особенность, Вы можете, например, настроить механизм ожидания появления подобного исключения в тесте, см. Пример 4.8.

Пример 4.8: Применение @expectedException для ожидяния ошибки PHP
<?php<br>
class ExpectedErrorTest extends PHPUnit_Framework_TestCase<br>
{<br>
    /**<br>
     * @expectedException PHPUnit_Framework_Error<br>
     */<br>
    public function testFailingInclude()<br>
    {<br>
        include 'not_existing_file.php';<br>
    }<br>
}<br>
?>
<br>
<br>
* This source code was highlighted with Source Code Highlighter.

phpunit ExpectedErrorTest
PHPUnit 3.4.2 by Sebastian Bergmann.

.

Time: 0 seconds

OK (1 test, 1 assertion)

PHPUnit_Framework_Error_Notice и PHPUnit_Framework_Error_Warning
представляют PHP уведомления и предупреждения.


Продолжение:
Часть 4
Поделиться публикацией
Ой, у вас баннер убежал!

Ну. И что?
Реклама
Комментарии 12
    +1
    Спасибо! все время лень было разбираться с этим PHPUnit (в первую очередь из-за того, что не понимал, как тестировать данные из БД), но сейчас вижу, сколько времени он может сэкономить.
      0
      У меня вопрос. В конструкторе класса инициализируется сессия и устанавливаются cookies.
      if (!session_id()) session_start();
      if(empty($_COOKIE['user_unid'])) setcookie(«user_unid», 'asd');

      При выполнении теста возникает ошибка (из-за того, то phpunit вывод начинает раньше)
      Cannot modify header information — headers already sent

      Как в этом случае поступить?
        +1
        мне кажется, надо старт сессии в тесты вписать, можно в конструктор, или в самый первый тест (ведь сессия тоже может и не стартануть), от которого все остальные зависят…
          0
          Я не полностью видимо сформулировал вопрос. Как быть с любыми заголовками (header), если они должны выдаваться в конструкторе класса?
          Как варианты мной придуманные:
          1) Отлавливать exception (как то не правильно)
          2) В параметрах конструктора прописывать переменную $testing (но тогда как быть, если эти заголовки действительно нужны для теста)
          3) Может как то отключить вывод сообщений самим phpunit до момента создания объекта тестируемого класса (не знаю как)
          4) Как то отделить поток вывода класса от потока вывода теста (тоже не знаю как)
          Больше пока ничего в голову не лезет.
            0
            Есть несколько способов:

            1. Dependency Injection
            Самый частый ответ в подобных топиках :)
            Инициализируйте сессию в «боевом» коде и передавайте результаты в конструктор. В тесте просто передавайте список в конструктор.
            Так даже намного удобней так как можно использовать @dataprovider.

            Боевой код:

            session_start();
            $class = new Class_A($_SESSION);

            Тест:
            $Session_test = array(smth...);
            $class = new Class_A($Session_test);

            2. Использовать метод setUpBeforeClass().
            Он запускается один раз перед всеми тестами. Но так вы далеко не уедете, ведь надо тестирова с разными данными в сессии :)

            Метод 1 универсален и используется на пример при тестировании сингтонов.
            Мне кажется что у вас проблемы с тестами. Заголовки мешать не должны. Сделайте метод и пускай он возвращает код в зависимости от инфы.
            Если вы хотите протестировать файловую систему то есть отдельные тесты,
            глава 11
            Mocking the Filesystem
            Stubbing and Mocking Web Services (WSDL)
              +2
              Правильный вариант — не использовать session_start() и _COOKIE. Это только в php работают напрямик с такими функциями, во всех нормальных языках и фреймворах для сессий и куков есть отдельные объекты, которые при тестах подменяются на поддельные.
            +2
            Используйте ob_start() перед началом тестирования. Весь поток вывода будет накапливаться в буфере и сбросится после завершения работы скрипта.
            +1
            Жаль что phpunit.ru был у hosting.ua… Теперь и не почитать русской документации =\
              +1
              Вот вроде бы с одной стороны и нужны тесты, писать проверять краевые условия и механизм работы, но с другой стороны для более менее серьезных дел, тесты писать становиться довольно сложнее, а следовательно человеко-часов тратится больше.
              С одной стороны да, так правильнее и лучше, с другой стороны дешевле нанять 1-2 тестеров, которые бы проверяли функционал кодера, а кодер занимался бы уже другими делами.

              Все выше сказанное естественно имхо.
                +1
                Автоматические тесты — это не только разовая проверка, это еще и существенная помощь при рефакторинге.
                  +3
                  Тесты должны сохранять деньги и время, но не наоборот.

                  На примере безопасности:
                  1. У вас на сайте нашли инъекцию через форму. Вы профиксили и сделали тест.
                  2. У вас снова нашли инъекцию с китайской точкой. Вы профиксили и тоже сделали тест.
                  3. Появляется мистическое японское тире.

                  Вы можете представить человека, который сделает такие тесты? :) Кстати человек сможет проверить далеко не все варианты.

                  Вспомните графы и способы обхода и поиска в них. Тесты это те же самые графы.

                  Тут вот много писалось про говнокод. Написать говнотест гораздо проще :)
                    +1
                    Собсно пример был к тому, что тесты позволяют исключить вероятность второго появления одной и той же ошибки в релизе. Даже если вы про эту ошибку давно уже забыли :)

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

                Самое читаемое