версия для печати

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

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

Поставщики данных (data providers)

Возьмем пример сложнее (см. полную версию в в архиве с исходниками). Приведенный ниже метод возвращает фразу в правильном склонении в зависимости от числа, идущего с текстом:

// src/Strings.php /** * Склонение слов в зависимости от числа * @param int $n число * @param array $s набор слов * @param bool $glued объединить результат с числом? Объединение будет через пробел * @return string */ public static function declination($n, array $s, $glued = true) { $n = $n % 100; $ln = $n % 10; $phrase = $s[(($n < 10 || $n > 20) && $ln >= 1 && $ln <= 4) ? (($ln == 1) ? 0: 1) : 2]; return $glued ? $n . " " . $phrase: $phrase; }

Тестовый метод (см. ):

// tests/StringsTest.php /** * Тест: склонение слов в зависимости от числа * * @dataProvider DeclinationProvider * @param int $num число * @param string $expect ожидаемый результат */ public function test_declination(int $num, string $expect) { $words = ["комментарий", "комментария", "комментариев"]; $result = Strings::declination($num, $words); $this->assertEquals($expect, $result, "Неверное склонение"); } /** * Данные: склонение слов в зависимости от числа * @return array */ public function DeclinationProvider() { return [ , , , ]; }

Data providers - это фишка PHPUnit, возможно есть аналоги в других фреймворках. К любому тест-методу можно присоединить поставщика данных через тег @dataProvider . Требования в методу-поставщику: он должен возвращать двумерный массив значений. Каждый подмассив - это набор данных на один тест-случай. Подмассив должен содержать элементы в том же количестве и порядке, как как это требуется в параметрах тест-метода.

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

Если в вашем поставщике данных возникнет исключение, тогда PhpUnit закончит тестирование с сообщением "No tests executed!" , что вообще ни о чем не говорит. Включайте отладчик и ищите, что у вас не так в DataProvider .

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

Прим.: в том же тестовом классе есть пример, как бы выглядел тест без использования DataProvider .

Еще один пример организации DataProvider см. в , метод test_checkAttrValueWithType() .

Важно : во время выполнения теста сначала вызывается поставщик данных, потом setUpBeforeClass() и setUp() (о них в следующем разделе ). Т.е. в dataProvider() нельзя полагаться на данные, которые могут быть заданы в setUpBeforeClass() | setup() тестового метода.

Фикстуры

Одной и наиболее времязатратных частей написания тестов является установка "мира" приложения в известное состоянии и откат к этому состоянию после теста. Это известное состояние называется фикстурой теста. (перевод из мануала PHPUnit).

PHPUnit позволяет организовывать тестовое окружение в отдельно взятом классе, а так же для каждого выполняемого теста в классе. Для этого есть группа методов, о которых подробно расписано в мануале PHPUnit: 4. Fixtures . Кратко расскажу.

Приведенные ниже методы (если они вам нужны для тестов) нужно реализовывать прямо в ваших тестовых классах:

  • сначала вызов всех методов поставщиков данных
  • setUpBeforeClass() статичный метод, выполняется на этапе создания тест-класса. Аналогичный ему статический метод tearDownAfterClass() выполняется после всех тестов
  • Динамический метод setUp() выполняется перед каждым тестом, tearDown() после каждого теста
  • Динамический метод assertPreConditions() выполняется перед первым утверждением в каждом тесте. assertPostConditions() выполняется, если тест успешно завершился.

Прим: найдите в мануале пример Example 4.2 и результат его выполнения. Этот пример показывает последовательность вызова всех методов фреймворка для установки фикстур.

Как использовать эти методы - решать вам. Идея простая: все, что можно инициализировать для всех/каждого тест-метода - выносят в методы setUpBeforeClass() и setUp() соответственно. Если после теста требуется уборка (например, очистка кеша), вызываются соответствующие методы отката.

Примеры использования в архиве исходников можно посмотреть в и . Содержимое этих методов может быть пока непонятно, но можно уловить мысль, что в них писать. Коротко: что-угодно общее для всех тест-методов.

Еще такой интересный момент: в unit-тестах можно принебречь оптимизацией или производительностью в пользу контролируемого поведения. Поэтому часто, вместо однократной инициализации окружения для всего класса через метод setUpBeforeClass() используют setUp() для создания известной среды для каждого тест-метода .

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

Подмена зависимостей

Подмена средствами PHPUnit

Тут мне трудно было выбрать, что рассказывать в статье, а за чем отправить в мануал PHPUnit: 9. Test Doubles . Там всего одна страница, и на примерах изложено вполне доступно. Я не хочу переписывать эту часть из документациии. Но все же скажу пару слов.

Как делается подмена зависимостей в PHPUnit 6.1. Напомню пример из теории (прим.: там класс назывался SomeClass ):

// в исходниках: src/ClassDI.php class ClassDI { /** * подключение к БД * @var IDatabase $db */ private $db; /** * Внедрение зависимости в класс * @param IDatabase $db подключение к БД */ public function __construct(IDatabase $db) { $this->$db = $db; } /** * Какой-то боевой метод * * Внедрение еще одной зависимости, прямо в метод * * @param ILogger $logger объект логера * @return int */ public function doIt(ILogger $logger):int { // тут только чистый код, без инициализации зависимых систем $id = $this->db->query("INSERT ..."); $logger->add("Создана новая запись #" . $id); return $id; } }

Тест с подменой зависимостей:

// в исходниках: tests/dummy_examples/ClassDITest.php public function test_doIt() { // Создаем поддельный объект зависимого класса $dbStub = $this->createMock(IDatabase::class); // Описываем ожидаемое поведение поддельного метода $dbStub->method("query") ->willReturn(10); // Подделываем другую зависимость, сразу указываем, какой метод подменяем. $loggerStub = $this ->getMockBuilder(ILogger::class) ->setMethods(["add"]) ->getMock(); $id = (new ClassDI($dbStub))->doIt($loggerStub); $this->assertEquals(10, $id); }

Собственно все.

Как видно из кода выше, для подделки использовали разные методы. Дело в том, что метод createMock() - это обертка. На самом деле, в нем выполняется цепочка методов PHPUnit:

$stub = $this ->getMockBuilder($originalClassName) ->disableOriginalConstructor() ->disableOriginalClone() ->disableArgumentCloning() ->disallowMockingUnknownTypes() ->getMock();

Все приведенные методы - публичные, поэтому при необходимости можно создать "тюнингованный" объект подделки. Так же можно вклиниться в эту цепочку, добавив вызовы других методов движка.

Подделки могут возвращать не только скалярные значения. У PHPUnit есть методы на все возможные случаи подмены.

Еще один пример подделки зависимости , метод test_validateAttributes() , блок кода Act .

В тест-методах можно строить утверждения относительно подмененного объекта (понятие mock помните?). И вот тут еще большее поле для деятельности. У меня нет простого, но сколь-нибудь полезного примера, поэтому лучше смотрите примеры в мануале PHPUnit. Mock Objects , если у вас возникнет такая необходимость. Как было отмечено в теоретической части статьи, проверка взаимодействий - это самый крайний случай в модульном тестировании, когда иначе никак проверить метод не получается.

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

Подмена с использованием Mockery

Документация

Mockery это PHP-фреймворк, запиленный специально для подмены объектов в unit-тестах. Он разработан, как альтернатива библиотеке подмены в PHPUnit , может использоваться в нем или как отдельный модуль, т.е. его можно подключить в другие тестовые движки. Основная фича - использование человекопонятного предметно-ориентированного языка (Domain-specific language, DSL).

Простым примером предметно-ориентированного языка является SQL для СУБД.

AspectMock проксирует все вызовы всех методов и позволяет их налету подменить. Я попробовал - это действительно круто. Но в итоге с моим движком он работать не смог, конфликтнул где-то. Т.о. возьмите на вооружение, если сможете подружить его с вашим проектом.

Установка

composer require --dev codeception/aspect-mock

Настройка. В bootstrap.php тестов прописываем типа этого (в исходниках см. ):

$kernel = \AspectMock\Kernel::getInstance(); $kernel->init([ "debug" => true, "includePaths" => [__DIR__ . "/../src"], "excludePaths" => [__DIR__ . "/../tests/"], "cacheDir" => __DIR__ . "/../temp/aspectMock/", ]);

В PHPUnit метод expectException() , а так же директива @expectedException используются в тестах для указания ядру фреймворка "ожидать такое-то исключение" . В итоге тест считается пройденным если исключение возникло.

И тут есть ньюансик: после того, как PHPUnit поймает ожидаемое исключение, выполнение тест-метода прекратится! Т.е. expectException() - это аналог assert-метода, только с прерыванием. Есть так же методы на проверку кода и сообщения исключения.

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

  1. забить на проверку исключений в принципе;
  2. писать тест-методы на нормальное поведение и на каждое пробрасываемое исключение отдельно;
  3. в тест-методе оформлять блоки try...catch ;
  4. использовать data provider .

Последний вариант мне представляется наиболее предпочтительным. Причем data provider позволяет описать вообще все тест-кейсы в одном методе.

См. скрипт архива исходников . Пример ExceptionsTest::test_normalizePriority() демонстрирует решение с data provider . Второй метод, test_getTargetFileName() для демонстрации исключения в одном методе вместе с нормальными ситуациями.

Пример отдельного теста только на проброс исключения см. в метод test_fuse_removeDir() . Исключение ожидается всего одно, data provider там не нужен. Но чтобы создать исключительную ситуацию в этом методе, требуется большая подготовка, поэтому тест-метод оформлен отдельно.

Тестирование запросов в базу данных

Тестирование взаимодействия с БД - это скорее интеграционный тест, но все же стоит один раз напрячься и сделать. Это несложно. Хотя зависит от проекта, я не настаиваю:)

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

В общих чертах: вам нужна будет еще одна база данных помимо боевой, структура должна соответствовать рабочей БД. Если используете миграции, поддержка актуальной структуры - не проблема. В PHPUnit нужно добавить модуль:

composer require --dev phpunit/dbunit

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

// tests/dummy_examples/DBEmptyTest.php use PHPUnit\Framework\TestCase; use PHPUnit\DbUnit\TestCaseTrait; use PHPUnit\DbUnit\DataSet\YamlDataSet; class MyGuestbookTest extends TestCase { use TestCaseTrait; /** * Соединение с тестовой базой * @return \PHPUnit\DbUnit\Database\DefaultConnection */ public function getConnection() { $pdo = new PDO("sqlite::memory:"); return $this->createDefaultDBConnection($pdo, ":memory:"); } /** * Загрузка данных в таблицы * @return YamlDataSet */ public function getDataSet() { return new YamlDataSet(__DIR__ . "/fixtures/dataset1.yml"); } }

Прим: реализация подключения к базе зависит от конкретного приложения. Методы getConnection() и getDataSet() выполняются перед каждым тест-методом.

Как это работает: PHPUnit подключается к базе, очищает таблицы и грузит в них данные, которые вы укажете. Ваш проверяемый класс должен уметь подключаться к тестовой БД. Далее обычная процедура тестирования - Arrange Act Assert . В конце - откат через , если требуется.

PHPUnit предоставляет кучу форматов для загрузки данных: несколько XML форматов, YAML, CSV, arrays и какие-то велосипеды. Имхо, удобнее всего YAML.

См. в архиве исходников пример подготовленных данных - , тест в

По моему мнению, этот модуль в PHPUnit еще сырой: слишком много возможностей, документация раскрывает не все, что есть. Видимо автор еще не определился.


Остальные части:


Понравилась статья? Расскажите о ней друзьям.

Это первая часть серии "PHPUnit для начинающих". В этом руководстве мы объясним для чего покрывать код unit-тестами и всю мощь инструмента PHPUnit. В конце мы напишем простой тест с использованием PHPUnit.
  • PHPUnit для начинающих. Часть 1: Начните использование.

Типы тестов

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

Давайте разделим тесты на категории по уровню их специфичности. По данным Википедии . В целом существует 4 признанных уровня тестов:

  • Unit-тестирование (модульное): этот уровень тестирует наименьшую единицу функциональности. С точки зрения разработчика его задачей является убедиться, что тестируемая функция выполняет именно то, для чего она реализована. Таким образом, она должна быть минимально зависима или совершенно независима от другой функции или класса. Она должна быть написана таким образом, чтобы она полностью выполнялась в памяти, т.е. она не должна коннектиться к БД, не должна обращаться к сети или использовать ФС и т.д. Unit-тестирование должно быть как можно более простым.
  • Интеграционное тестирование: этот уровень "соединяет" разные единицы кода и тестирует правильно ли работают их комбинации. Он надстраивается сверху над unit-тестированием и способен отловить баги, которые нельзя выявить с помощью unit-тестирования, т.к. интеграционное тестирование проверяет, работает ли класс А с классом Б.
  • Системное тестирование: оно создано для воспроизведения работы сценариев в условиях, приближенных к боевым. Оно, в свою очередь, надстраивается сверху над интеграционным тестированием. В то время как интеграционное тестирование обеспечивает слаженную работу различных частей системы. Системное тестирование отвечает за то, что система работает так, как предполагает пользователь, прежде чем отправить её следующий уровень.
  • Приёмочное тестирование: в то время как выше приведённые тесты предназначены для разработчиков на стадии разработки, приёмочное тестирование фактически выполняется пользователями ПО. Пользователей не интересуют внутренние особенности ПО, их интересует только как работает это ПО.

Если мы поместим типы тестов в пирамиду, выглядеть это будет так:

Что такое PHPUnit

Глядя на пирамиду вверху, мы можем сказать, что юнит-тестирование — это строительный материал для всех остальных видов тестирования. Когда мы закладываем сильную базу, мы способны построить устойчивое приложение. Однако написание тестов вручную, а также запуск тестов каждый раз когда вы вносите изменения — это процесс трудоёмкий. Если бы существовал инструмент для автоматизации этого процесса, то написание тестов стало бы гораздо более приятным занятием.

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

Давайте установим PHPUnit в нашей системе:

  1. Загрузите его: PHPUnit распространяется в PHAR(PHp ARhive) файле. Скачать можно .
  2. Добавьте путь к нему в системную переменную $PATH: после скачивания PHAR файла, убедитесь, что он является запускаемым (executable) и путь, где он находится, прописан в системной переменной $PATH . Т.о. вы сможете запускать его из любого места.

Если вы работаете на Unix-подобной системе, то это вы можете сделать следующими командами:

$ wget https://phar.phpunit.de/phpunit.phar $ chmod +x phpunit.phar $ sudo mv phpunit.phar /usr/local/bin/phpunit

Если вы сделали всё верно, то вы сможете увидеть версию установленного PHPUnit, набрав в вашем терминале команду:

$ phpunit --version

Ваш первый unit-тест

Пришло время написать ваш первый unit-тест! Для начала нам нужен какой-нибудь класс, который мы будем тестировать. Давайте напишем простенький класс под названием Calculator . И напишем для него тест.

Создайте файл "Calculator.php" и скопируйте в него нижеприведённый код. Этот класс Calculator имеет только один метод add .

Class Calculator { public function add($a, $b) { return $a + $b; } }

Теперь создайте файл для тестов "CalculatorTest.php" и скопируйте в него следующий код. Мы остановимся на каждом методе более детально.

Require "Calculator.php"; class CalculatorTests extends PHPUnit_Framework_TestCase { private $calculator; protected function setUp() { $this->calculator = new Calculator(); } protected function tearDown() { $this->calculator = NULL; } public function testAdd() { $result = $this->calculator->add(1, 2); $this->assertEquals(3, $result); } }

  • Line 2: подключаем файл тестируемого класса Calculator.php . Так как в этом файле мы собираемся тестировать именно этот класс, убедитесь, что он подключен.
  • Line 8: setUp() это метод, который вызывается перед каждым тестом. Запомните он вызывается перед Каждым тестом , что означает, что если вы добавите ещё один метод теста в этот класс, то он будет вызываться и перед ним тоже.
  • Line 13: аналогично методу setUp() , tearDown() вызывается после каждого теста.
  • Line 18: testAdd() — это метод-тест для метода add() . PHPUnit будет распознавать каждый метод, начинающийся с test, как метод-тест и автоматически запускать его. В действительности этот метод очень прост: сначала мы вызываем метод Calculator::add() чтобы вычислить значение 1 плюс 2, а затем мы проверяем, что этот метод вернул правильное значение, используя assertEquals() из PHPUnit.

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

$ phpunit CalculatorTest.php

Если вы всё сделали правильно, то вы должны увидеть что-то вроде этого:

PHPUnit 3.7.32 by Sebastian Bergmann. . Time: 31ms, Memory: 2.25Mb OK (1 test, 1 assertion)

Заключение

Мы завершили первое руководство из серии "PHPUnit для начинающих". В следующей статье мы собираемся показать вам как использовать Data Provider (поставщик данных) в ваших тестах.

Надеемся это простое руководство поможет вам в вашей разработке и поможет начать использовать unit-тестирование.

Если вам понравился перевод на эту тему читайте нас в

Так как генерация исключения в коде приложения является частым явлением, рассмотрим как это дело можно тестировать с помощью PHPUnit.
В старых статьях на эту тему можно встретить использование метода setExpectedException() , но имейте ввиду, что в новых версиях phpUnit используется метод expectException() для указания типа ожидаемого исключения.

Метод expectException(), а так же директива @expectedException используются в тестах для указания "ожидать такое-то исключение". Тест считается пройденным, если возникло исключение указанного типа.

Пример.
Исключение возникает если в переданном параметре менее 4-х символов:
class User { public function verifyPassword(array $user){ if(strlen($user["password"])<4){ throw new LengthException("Количество символов в пароле (" .$user["password"]. ") не соответствует требованиям."); } // } }
Тестируем:
class UserTest extends TestCase { public function testVerifyPassword() { $obj = new User(); $user["password"] = "123"; $this->expectException(LengthException::class); $obj->verifyPassword($user); } }
данный код можно было бы переписать так:
class UserTest extends TestCase { /** * @expectedException LengthException */ public function testVerifyPassword() { $obj = new User(); $user["password"] = "123"; $obj->verifyPassword($user); } } т.е. использовать директиву @expectedException с указанием нужного исключения.

ВАЖНО! После того, как PHPUnit поймает ожидаемое исключение, выполнение данного тестирующего метода прекратится!
Другими словами - если в одном методе у вас 2 или больше тестов, например:
$this->expectException(LengthException::class); $this->assertEquals(8, $result, "Двойка в третьей степени"); то при выполнении первого теста с исключением, второй будет пропущен (при условии что исключение будет получено). Поэтому нужно размещать тесты исключений в разных методах или:
- в тест-методе оформлять блоки try...catch;
- использовать data provider.

Анализ покрытия кода тестами.

При большом количестве классов можно забыть протестировать какие-то методы или разные варианты возвращаемых ими результатов. Так же можно что-то отложить на потом или вообще вдруг решить тестировать то, что до этого не собирались. Как же оперативно проверить что уже было протестировано, а что нет?!! В PHPUnit для этого используется инструмент php-code-coverage .

Для использования нужно предварительно подключить php-расширение Xdebug в файле интерпретатора php - php.ini.
В OpenServer файл php.ini меняется так:
меню -> Дополнительно -> Конфиграция -> PHP
Откроется файл конфигурации, где необходимо раскомментировать строку (удалить спереди точку с запятой):
;zend_extension="e:/openserver/modules/php/PHP-5.6/ext/php_xdebug.dll"
после внесения изменений не забыть перезагрузить сервер.

Проверить подключено ли расширение, выполнить скрипт с командой:
phpinfo(); или открыть ссылку http://127.0.0.1/openserver/phpinfo.php
должен присутствовать блок «xdebug ».

Выполнение:
phpunit --coverage-html tests\coverage тут указываем, что отчет нужно предоставить в виде статичных html файлов и разместить их в папке coverage, которая появится в каталоге tests.

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

Красной строкой выделен фрагмент упущенный при создании тестов. Т.е., в данном случае, нужно написать тест при котором данный метод будт возвращать значение false.
Если при выполнении команды возникнет ошибка:
No whitelist configured, no code coverage will be generated
то нужно изменить/создать в корне проекта файл phpunit.xml и вставить туда блок с указанием каталога который нужно проверять:
app тут указываем, что нужно проверить покрытие кода тестами в php файлах из каталога app .

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

При работе с Composer в командной строке, при включенном php-расширение Xdebug , может появляться сообщение:
You are running composer with xdebug enabled. This has a major impact on runtime performance. See https://getcomposer.org/xdebug
В котором сказано, что включенное расширение xdebug оказывает существенное влияние на производительность (снижает). Как решить эту проблему читайте в моей .

В следующей статье я расскажу как работать с имитирующими объектами (моками), чем завершим серию статей по основам использования PHPUnit.

Вы, ребята, проводите модульное тестирование на PHP? Я не уверен, что я когда-либо делал это... что это такое?

assertEquals(0, count($stack)); array_push($stack, "foo"); $this->assertEquals("foo", $stack); $this->assertEquals(1, count($stack)); $this->assertEquals("foo", array_pop($stack)); $this->assertEquals(0, count($stack)); } } ?>

В качестве более сложного примера я хотел бы указать вам на фрагмент my на github .

PHPUnit с охватом кода

Мне нравится практиковать что-то под названием TDD с использованием модульной системы тестирования (в PHP, которая phpunit).

Что мне также очень нравится в phpunit , так это то, что он также предлагает покрытие кода через xdebug. Как видно из изображения ниже, мой класс имеет 100% -ный охват тестирования. Это означает, что была проверена каждая строка из моего класса Authentication , что дает мне уверенность в том, что код делает то, что должен. Имейте в виду, что покрытие не всегда означает, что ваш код хорошо протестирован. Вы можете иметь 100% -ый охват без тестирования отдельной строки производственного кода.

Netbeans

Лично мне нравится тестировать свой код внутри Netbeans (для PHP). с простым щелчком мыши (alt + f6) я могу проверить весь свой код. Это означает, что мне не нужно оставлять IDE, что мне очень нравится, и помогает экономить время переключения между сеансами.


Close