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

Для достижения этих преимуществ модульные тесты в идеале должны охватывать все возможные пути исполнения программы. Один модульный тест обычно покрывает один конкретный путь в одной функции или методе. Однако тестовые методы необязательно должны быть инкапсулированными и независимыми.

Часто существуют неявные зависимости между тестовыми методами, скрытые в сценарии реализации теста.


Установка PHPUnit и первый тест

Установка через Composer:

composer require --dev phpunit/phpunit

Создаем пример для теста Duck.php:

class Duck {
    public function say() {
        return 'krya-krya';
    }
}

Создаем папку tests и добавляем туда тестируем класс DuckTest.php (по умолчанию PHPUnit смотрит на все файлы, которые заканчиваются на Test.php).

require_once __DIR__ . '/../Duck.php';
 
class Krya_Test extends \PHPUnit\Framework\TestCase {
    public function test_say() {
        $krya = new Duck();
        $this->assertSame( 'krya-krya', $krya->say() );
    }
}

И запускаем тесты:

C:\xampp\htdocs\test.com\vendor\bin\phpunit tests

Если вы увидели такое сообщение, то все сделано верно.

PHPUnit 5.7.27 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 128 ms, Memory: 2.25MB

OK (1 test, 1 assertion)

Фикстуры (Fixture)

Фиксутра - настройка окружения и возврат его в исходное состояние. Звучит страшно, но на самом деле это просто методы, которые позволяют настроить окружение для теста. В классе \PHPUnit\Framework\TestCase для этого есть методы:

  • setUp - выполняется перед каждым тестом тестового класса;
  • tearDown - выполняется после каждого теста тестового класса;
  • setUpBeforeClass - выполняется перед первым тестом в тестовом классе;
  • tearDownAfterClass - выполняется после запуска последнего теста тестового класса.

Пример:

class FixtureTest extends \PHPUnit\Framework\TestCase {
    private $counter;
   
    protected function setUp() {
        $this->counter = 0;
        parent::setUp();
    }
 
    public function test_fixture_1() {
        $this->counter++;
        $this->assertSame( 1, $this->counter );
    }
 
    public function test_fixture_2() {
        $this->assertSame( 0, $this->counter );
    }
}

Получается в каждом тестируемом методе свойство counter будет равно 0.


Утверждение (Asserts)

Утверждения - это методы класса TestCase, которые помогают проверить тест. Все эти методы начинаются с assert (assertTrue, assertSame, assertClassHasAttribute и т.д.). После выполнения тестов будет показано количество выполненных тестов и количество выполненных утверждений. В случае неверного утверждения тест считается проваленным.


Тестовые двойники (Заглушки, Stubs, Mocks)

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

use PHPUnit\Framework\TestCase;
 
class StubTest extends TestCase {
    public function testStub() {
        $headdress = $this->createMock(Headdress::class);
        $duck = new Duck( $headdress );
 
        $this->assertSame('foo', $duck->quack() );
    }
}

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

use PHPUnit\Framework\TestCase;
 
class MockTest extends TestCase {
    public function testMock() {
        $headdress = $this->createMock(Headdress::class);
        $headdress->expects( 'back' );
        $duck = new Duck( $headdress );
 
        $this->assertTrue( $duck->run() );
    }
}

В нашем примере мы еще описали, что ожидаем, что Duck при вызове метода run вызовет внутри себя метод back объекта Headdress.


Настройка окружения для всех тестов (Bootstrap)

Для настройки окружения для всех тестов можно создать файл bootstrap.php и объявить там необходимые вещи. Например, в этом файле мы можем подключить библиотеки необходимые для тестирования, изменить глобальные и супер-переменные, объявить константы и т.д.

Пример bootstrap.php файла:

define( 'PLUGIN_PATH', __DIR__ '/../' );
 
require_once PLUGIN_PATH . '/vendor/autoload.php';

Конфигурационный XML-файл

Мы можем настроить выполнение команд с помощью конфигурационного XML-файла, в котором можно изменить стандартные настройки phpunit, указать папку с тестами, путь к bootstrap файлу, настроить фильтры и другое.

Пример XML-файла:

<phpunit bootstrap="./bootstrap.php" colors="true">
    <testsuites>
        <testsuite name="Config-example-for-tests">
            <directory suffix=".php">./tests/</directory>
        </testsuite>
    </testsuites>
    <filter>
        <whitelist>
            <directory suffix=".php">../../</directory>
            <exclude>
                <directory>../../src</directory>
                <directory>../../tests</directory>
                <directory>../../vendor</directory>
            </exclude>
        </whitelist>
    </filter>
</phpunit>

Атрибутами или вложенными тегами к <phpunit> могут быть любые команды, которые есть в phpunit. В нашем случае:

vendor/bin/phpunit /tests/ --bootsrap /tests/bootsrap.php --colors --test-suffix=".php" ...

Меняется на:

vendor/bin/phpunit --configuration phpunit.xml

Покрытие тестами (Tests coverage)

С помощью этого инструмента очень легко понять насколько качественно написаны тесты, сколько файлов покрыты тестами и какие строки в них покрыты. Данный инструмент очень сильно помогает отслеживать качество ваших тестов.

Необходимо к вашему php-cli подключить xdebug иначе coverage будет недоступен и вы получите уведомление об отсутствии модуля для тестирования.

Чтобы его использовать достаточно дописать к phpunit --coverage-{type} атрибут. Например очень удобно использовать HTML-формат покрытия:

vendor/bin/phpunit --configuration phpunit.xml --coverage-html coverage

После этого в проекте создается папка coverage и мы можем открыть файл index.html и просмотреть подробную информацию о каждом файле тестирования.