Если вы в очередной раз находите себя в ситуации, когда пишете скрипт примерно такого вида:

<?php
include 'app/common.php';
assert_options(ASSERT_BAIL, true);

$instance = new ExampleClass();
$instance->setValue(42);

assert($instance->getValueSquared() == 1764);

try {
    $instance->getResult();
} catch (Exception $e) {
    assert($e instanceof SpecificException);
}

echo "OK\n";

И если вы уже начинаете забывать, где какой скрипт для тестирования чего (не говоря уж о ваших коллегах), то это означает что пришло время вам внести структуру в ваши тесты. Поможет вам в этом PHPUnit — фреймворк для написания юнит-тестов.

Установка

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

composer require --dev phpunit/phpunit

Проверим корректность установки PHPUnit, запросив версию:

php vendor/bin/phpunit --version

Настройка

Понятно, что вы не хотите каждый раз вручную указывать всевозможные аргументы типа --bootstrap при запуске phpunit, а потому лучше всего будет сразу же, до начала работы, указать все необходимые параметры для системного запуска тестов. Настроил — и забыл!

Настройки PHPUnit хранятся в XML-файле в корне вашего проекта. Лучше сразу использовать phpunit.xml.dist, а не phpunit.xml, чтобы у ваших коллег была возможность создать свою подборку тестов.

Что мы делаем

Пример минимального файла phpunit.xml.dist, который подразумевает, что автозагрузчик и всё необходимо для работы наших классов инициализируется в app/common.php, и что все тесты лежат в каталоге tests в корне проекта, и в его подкаталогах.

<?xml version="1.0" encoding="utf-8"?>
<phpunit bootstrap="app/common.php" colors="true">
  <testsuites>
    <testsuite name="Main">
      <directory>tests</directory>
    </testsuite>
  </testsuites>
</phpunit>

Если никакая инициализация кроме автозагрузчика не нужна, то вместо app/common.php можно и нужно использовать vendor/autoload.php который генерирует Composer.

Вас никто не обязывает держать тесты в каком-то одном месте, хоть это и фактический стандарт для PHP библиотек. Вы вполне можете включить вообще все исходные файлы вашего проекта в область поиска тестов, например, добавив ещё один <testsuite>, или задав дополнительные каталоги для поиска тестов добавив ещё один <directory> в существующий <testsuite>.

Подход с включением всего подряд в область поиска тестов имеет определённые недостатки в том, что касается оценки покрытия кода тестами, но не будем пока на этом останавливаться — нам важно сделать хоть что-то.

Что мы получили

С указанным выше минимальным phpunit.xml.dist будут просмотрены все файлы, заканчивающиеся на *Test.php, и использованы все найденные в этих файлах классы, названия которых заканчиваются на *Test.

Примерно оценить список классов, которые будут задействованы, можно так:

find tests/ -type f -name *Test.php -print0 |
  xargs -0 grep --color 'class .*Test '

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

php vendor/bin/phpunit

Применение

Самый простой вид теста представляет собой...

  • Метод класса, начинающийся на test,
  • в классе, заканчивающемся на Test,
  • и наследующем от PHPUnit_Framework_TestCase.

Понимаю, сложно и запутанно. Но посмотрим на пример такого класса:

<?php
class ExampleTest extends PHPUnit_Framework_TestCase
{
    public function testExample()
    {
        $this->assertTrue(true);
    }
}

Как видите, ничего особенно сложного.

Достаточно будет определить этот класс в tests/ExampleTest.php и мы сможем испытать PHPUnit:

php vendor/bin/phpunit tests/ExampleTest.php

Эта команда сообщит нам об успешном прохождении всех тестов:

PHPUnit 5.X.X by Sebastian Bergmann.

Configuration read from phpunit.xml

.

Time: 63 ms, Memory: 6.50Mb

OK (1 test, 1 assertion)

Практический пример

Чтобы не ходить далеко, перепишем скрипт из начала этой заметки.

<?php
class ExampleClassTest extends PHPUnit_Framework_TestCase
{
    public function testValueSquared()
    {
        $instance = new ExampleClass();
        $instance->setValue(42);
        $this->assertEquals(1764, $instance->getValueSquared());

        return $instance;
    }

    /**
     * @depends testValueSquared
     * @expectedException SpecificException
     */
    public function testInvalidResult($instance)
    {
        $instance->getResult();
    }
}

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

Испытаем:

$ php vendor/bin/phpunit tests/ExampleClassTest.php 
PHPUnit 5.X.X by Sebastian Bergmann and contributors.

..                                      2 / 2 (100%)

Time: 75 ms, Memory: 8.50Mb

OK (2 tests, 2 assertions)

Вот так могло бы выглядеть определение для тестируемого класса ExampleClass:

<?php
class ExampleClass
{
    private $value;

    public function setValue($value)
    {
        $this->value = $value;
    }

    public function getValueSquared()
    {
        return pow($this->value, 2);
    }

    public function getResult()
    {
        throw new SpecificException();
    }
}

class SpecificException extends Exception
{
}

Что дальше?

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

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

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