На нашем сайте уже есть много постов PHPUnit (просто отметьте тег ), но прошло много времени с тех пор, как мы на самом деле знакомили людей с ним, и с тех пор этот инструмент значительно развился.
Эта статья направлена на то, чтобы повторно представить инструмент современным способом, современной аудитории, в современной среде PHP — если вы не знакомы с PHPUnit или тестированием, этот пост для вас.
Здесь мы предполагаем, что вы знакомы с объектно-ориентированным PHP и используете PHP версии 7 и выше. Для запуска и запуска среды, в которой предустановлен PHP 7, и чтобы можно было следовать инструкциям, приведенным в этом сообщении, без каких-либо ошибок, мы рекомендуем использовать Homestead Improved . Также обратите внимание, что ожидается некоторое использование командной строки, но вы будете руководствоваться всем этим. Не бойтесь этого — это инструмент более мощный, чем вы можете себе представить.
Если вам интересно, почему мы рекомендуем всем использовать Vagrant, я подробно расскажу об этом в среде PHP Start Start , но это введение Vagrant также объяснит все адекватно.
Что такое разработка через тестирование?
Разработка через тестирование — это идея, что вы пишете код таким образом, что сначала вы пишете еще один кусочек кода, единственная цель которого — убедиться, что изначально предназначенный код работает, даже если он еще не написан.
Проверка того, является ли что-то действительно тем, что мы ожидаем, называется утверждением на TDD-земле. Запомните этот термин.
Например, утверждение, что 2 + 2 = 4 является правильным. Но если мы утверждаем, что 2 + 3 = 4, среда тестирования (например, PHPUnit) пометит это утверждение как ложное. Это называется «проваленный тест». Мы проверили 2 + 3 4, и не удалось. Очевидно, что в вашем приложении вы не будете проверять суммы скалярных значений — вместо этого будут переменные, которые язык заменит во время выполнения на реальные значения и подтвердит это, но вы поняли идею.
Что такое PHPUnit?
PHPUnit — это набор утилит (классы PHP и исполняемые файлы), которые не только облегчают написание тестов (написание тестов часто влечет за собой написание большего количества кода, чем на самом деле имеет приложение — но оно того стоит), но также позволяет вам увидеть результат работы процесс тестирования в хорошем графике, который позволяет узнать о качестве кода (например, может быть, слишком много IF в классе — это помечено как плохое качество, потому что изменение одного условия часто требует переписать столько тестов, сколько имеется IF), охват кода (насколько данного класса или функции были охвачены тестами, и сколько остается непроверенным), и многое другое.
Чтобы не утомлять вас слишком большим количеством текста (слишком поздно?), Давайте на самом деле используем его и учимся на примерах.
Код, который мы заканчиваем в конце этого урока, можно скачать с Github .
Начальная загрузка примера приложения
Чтобы привести примеры домой, мы создадим простой пакет командной строки, который позволит пользователям превращать файл JSON в файл PHP. Этот файл PHP будет содержать данные JSON в виде ассоциативного массива PHP. Это только мой личный пример использования — я часто использую Diffbot, и результат может быть огромным — слишком большой, чтобы его можно было проверить вручную, поэтому более удобная обработка с помощью PHP может оказаться очень полезной.
Впредь предполагается, что вы используете полностью PHP 7-совместимую среду с установленным Composer и можете следовать за ней. Если вы загрузили Homestead Improved , пожалуйста, используйте SSH сейчас с помощью vagrant ssh
, и давайте начнем.
Сначала мы перейдем в папку, где живут наши проекты. В случае Homestead Improved это Code
.
cd Code
Затем мы создадим новый проект на основе PDS-Skeleton и установим внутри него PHPUnit с помощью Composer .
git clone https://github.com/php-pds/skeleton converter cd converter composer require phpunit/phpunit --dev
Обратите внимание, что мы использовали флаг --dev
чтобы установить PHPUnit только в качестве зависимости dev — это означает, что он не нужен в производственной --dev
что --dev
наш развернутый проект легким. Также обратите внимание, что тот факт, что мы начали с PDS-Skeleton, означает, что наша папка tests
уже создана для нас с двумя демонстрационными файлами, которые мы будем удалять.
Далее нам нужен фронт-контроллер для нашего приложения — файл, через который перенаправляются все запросы. В converter/public
создайте index.php
со следующим содержимым:
<?php echo "Hello world";
Вы должны быть знакомы со всем вышеуказанным содержанием. С нашим содержимым «Hello World», давайте удостоверимся, что мы можем получить к нему доступ из браузера.
Если вы используете Homestead Improved , я надеюсь, что вы следовали инструкциям и настраивали виртуальный хост или обращаетесь к приложению через IP-адрес виртуальной машины.
Давайте удалим лишние файлы сейчас. Либо сделайте это вручную, либо выполните следующее:
rm bin/* src/* docs/* tests/*
Вы можете быть удивлены, зачем нам нужен фронт-контроллер с Hello World. Мы не будем использовать его в этом уроке, но позже, когда мы будем тестировать наше приложение на людях, оно пригодится. В любом случае, он не будет частью окончательного пакета, который мы развернем.
Комплекты и конфигурации
Нам нужен конфигурационный файл PHPUnit, который сообщает PHPUnit, где искать тесты, какие подготовительные шаги нужно выполнить перед тестированием и как тестировать. В корне проекта создайте файл phpunit.xml
со следующим содержимым:
<phpunit bootstrap="tests/autoload.php"> <testsuites> <testsuite name="converter"> <directory suffix="Test.php">tests</directory> </testsuite> </testsuites> </phpunit>
во<phpunit bootstrap="tests/autoload.php"> <testsuites> <testsuite name="converter"> <directory suffix="Test.php">tests</directory> </testsuite> </testsuites> </phpunit>
во<phpunit bootstrap="tests/autoload.php"> <testsuites> <testsuite name="converter"> <directory suffix="Test.php">tests</directory> </testsuite> </testsuites> </phpunit>
phpunit.xml
Проект может иметь несколько наборов тестов, в зависимости от контекста. Например, все, что связано с учетной записью пользователя, может быть сгруппировано в набор, называемый «пользователи», и он может иметь свои собственные правила или другую папку для тестирования этой функциональности. В нашем случае, проект очень маленький, поэтому одного набора более чем достаточно, ориентированного на каталог tests
. Мы определили suffix
аргумент — это означает, что PHPUnit будет запускать только те файлы, которые заканчиваются на Test.php
. Полезно, когда мы хотим, чтобы в tests
другие файлы, но мы не хотим, чтобы они запускались, за исключением случаев, когда мы вызываем их из реальных файлов Test.
О других подобных аргументах вы можете прочитать здесь .
Значение bootstrap
сообщает PHPUnit, какой файл PHP загружать перед тестированием. Это полезно при настройке переменных автозагрузки или тестирования проекта, даже базы данных тестирования и т. Д. — всего, что вам не нужно или не нужно в рабочем режиме. Давайте создадим tests/autoload.php
:
<?php require_once __DIR__.'/../vendor/autoload.php';
tests/autoload.php
В этом случае мы просто загружаем автозагрузчик Composer по умолчанию, поскольку PDS-Skeleton уже настроил для нас пространство имен Tests в composer.json
. Если мы заменим значения шаблона в этом файле нашими собственными, мы получим файл composer.json
который выглядит следующим образом:
{ "name": "sitepoint/jsonconverter", "type": "standard", "description": "A converter from JSON files to PHP array files.", "homepage": "https://github.com/php-pds/skeleton", "license": "MIT", "autoload": { "psr-4": { "SitePoint\\": "src/SitePoint" } }, "autoload-dev": { "psr-4": { "SitePoint\\": "tests/SitePoint" } }, "bin": ["bin/converter"], "require-dev": { "phpunit/phpunit": "^6.2" } }
После этого мы запускаем composer du
(сокращение от dump-autoload
), чтобы обновить сценарии автозагрузки.
composer du
Первый тест
Помните, что TDD — это искусство сначала делать ошибки, а затем вносить изменения в код, чтобы они перестали быть ошибками, а не наоборот. Имея это в виду, давайте создадим наш первый тест.
<?php namespace SitePoint\Converter; use PHPUnit\Framework\TestCase; class ConverterTest extends TestCase { public function testHello() { $this->assertEquals('Hello', 'Hell' . 'o'); } }
tests/SitePoint/Converter/ConverterTest.php
Лучше всего, если тесты будут следовать той же структуре, что и наш проект. Имея это в виду, мы даем им одинаковые пространства имен и одинаковое расположение дерева каталогов. Таким образом, наш файл ConverterTest.php
находится в tests
, подпапка SitePoint
, подпапка Converter
.
Расширяемый файл является самой базовой версией класса Test, которую предлагает PHPUnit. В большинстве случаев этого будет достаточно. Когда нет, это прекрасно, чтобы расширить его, а затем основываться на этом. Помните — тесты не должны следовать правилам хорошего проектирования программного обеспечения, поэтому глубокое наследование и повторение кода хороши — до тех пор, пока они тестируют то, что нужно тестировать!
В этом примере «контрольный пример» утверждает, что строка Hello
равна объединению Hell
и o
. Если мы сейчас запустим этот пакет с php vendor/bin/phpunit
, мы получим положительный результат.
PHPUnit запускает каждый метод, начиная с
test
вTest
файле, если не указано иное. Вот почему нам не нужно было явно указывать при запуске набора тестов — все это автоматически.
Наш текущий тест не является ни полезным, ни реалистичным. Мы использовали это просто, чтобы проверить, работает ли наша установка. Давайте напишем правильный сейчас. Перепишите файл ConverterTest.php
следующим образом:
<?php namespace SitePoint\Converter; use PHPUnit\Framework\TestCase; class ConverterTest extends TestCase { public function testSimpleConversion() { $input = '{"key":"value","key2":"value2"}'; $output = [ 'key' => 'value', 'key2' => 'value2' ]; $converter = new \SitePoint\Converter\Converter(); $this->assertEquals($output, $converter->convertString($input)); } }
tests/SitePoint/Converter/ConverterTest.php
Хорошо, так что здесь происходит?
Мы тестируем «простое» преобразование. Входные данные представляют собой строку JSON, строковый объект, а ожидаемый результат — его версию массива PHP. Наш тест утверждает, что наш класс Converter при обработке $input
с convertString
метода convertString
производит желаемый $output
, как определено.
Перезапустите набор.
Неудачный тест! Ожидаемый, так как класс даже еще не существует.
Давайте сделаем вещи немного более драматичными — с цветом! Отредактируйте файл phpunit.xml
так, <phpunit
тег <phpunit
содержал colors="true"
, например так:
<phpunit colors="true" bootstrap="tests/autoload.php">
Теперь, если мы запустим php vendor/bin/phpunit
, мы получим более впечатляющий результат:
Прохождение теста
Теперь мы начинаем процесс прохождения этого теста.
Наша первая ошибка: «Класс« SitePoint \ Converter \ Converter »не найден». Давайте это исправим.
<?php namespace SitePoint\Converter; class Converter { }
src/SitePoint/Converter/Converter.php
;
Теперь, если мы повторно запустим пакет …
Прогресс! Мы пропустили метод, который мы использовали сейчас. Давайте добавим это в наш класс.
<?php namespace SitePoint\Converter; class Converter { public function convertString(string $input): ?array { } }
src/SitePoint/Converter/Converter.php
;
Мы определили метод, который принимает входные данные типа строки и возвращает либо массив, либо ноль, если не удалось. Если вы не знакомы со скалярными типами ( string $input
), узнайте больше здесь , а о типах возвращаемых значений ( ?array
) смотрите здесь .
Перезапустите тесты.
Это ошибка возвращаемого типа — функция ничего не возвращает (void), потому что она пустая, и ожидается, что она возвратит либо ноль, либо массив. Давайте завершим метод. Мы будем использовать встроенную в json_decode
функцию json_decode
для декодирования строки JSON.
public function convertString(string $input): ?array { $output = json_decode($input); return $output; }
src/SitePoint/Converter/Converter.php
;
Давайте посмотрим, что произойдет, если мы повторно запустим пакет.
Ооо Функция возвращает объект, а не массив. Ах, ха! Это потому, что мы не активировали режим «ассоциативный массив» в функции json_decode
. Функция превращает массивы JSON в экземпляры stdClass
по умолчанию, если не указано иное. Измените это так:
public function convertString(string $input): ?array { $output = json_decode($input, true); return $output; }
src/SitePoint/Converter/Converter.php
;
Перезапустите набор.
Ура! Наш тест сейчас проходит! Он получит точно такой же результат, какой мы ожидаем от него в тесте!
Давайте добавим еще несколько тестов, чтобы убедиться, что наш метод действительно работает так, как задумано. Давайте сделаем это немного сложнее, чем простой пример, с которого мы начали. Добавьте следующие методы в ConverterTest.php
:
{ $input = '{"key":"value","key2":"value2","some-array":[1,2,3,4,5]}'; $output = [ 'key' => 'value', 'key2' => 'value2', 'some-array' => [1, 2, 3, 4, 5], ]; $converter = new \SitePoint\Converter\Converter(); $this->assertEquals($output, $converter->convertString($input)); } public function testMoreComplexConversion() { $input = '{"key":"value","key2":"value2","some-array":[1,2,3,4,5],"new-object":{"key":"value","key2":"value2"}}'; $output = [ 'key' => 'value', 'key2' => 'value2', 'some-array' => [1, 2, 3, 4, 5], 'new-object' => [ 'key' => 'value', 'key2' => 'value2', ], ]; $converter = new \SitePoint\Converter\Converter(); $this->assertEquals($output, $converter->convertString($input)); } public function testMostComplexConversion() { $input = '[{"key":"value","key2":"value2","some-array":[1,2,3,4,5],"new-object":{"key":"value","key2":"value2"}},{"key":"value","key2":"value2","some-array":[1,2,3,4,5],"new-object":{"key":"value","key2":"value2"}},{"key":"value","key2":"value2","some-array":[1,2,3,4,5],"new-object":{"key":"value","key2":"value2"}}]'; $output = [ [ 'key' => 'value', 'key2' => 'value2', 'some-array' => [1, 2, 3, 4, 5], 'new-object' => [ 'key' => 'value', 'key2' => 'value2', ], ], [ 'key' => 'value', 'key2' => 'value2', 'some-array' => [1, 2, 3, 4, 5], 'new-object' => [ 'key' => 'value', 'key2' => 'value2', ], ], [ 'key' => 'value', 'key2' => 'value2', 'some-array' => [1, 2, 3, 4, 5], 'new-object' => [ 'key' => 'value', 'key2' => 'value2', ], ], ]; $converter = new \SitePoint\Converter\Converter(); $this->assertEquals($output, $converter->convertString($input)); }
tests/SitePoint/Converter/ConverterTest.php
Мы сделали каждый тест немного более сложным, чем предыдущий, с последним, содержащим несколько объектов в массиве. Повторный запуск набора тестов показывает нам, что все в порядке …
… но что-то не так, не так ли? Здесь очень много повторений, и если мы когда-нибудь изменим API класса, нам придется внести изменения в 4 местах (пока). Преимущества DRY начинают проявляться даже в тестах. Ну, есть возможность помочь с этим.
Поставщики данных
Поставщики данных — это специальные функции в тестовых классах, которые имеют одну конкретную цель: предоставить набор данных для тестовой функции, чтобы вам не нужно было повторять ее логику для нескольких тестовых функций, как мы это делали. Это лучше всего объяснить на примере. Давайте сделаем рефакторинг нашего класса ConverterTest
следующим образом:
<?php namespace SitePoint\Converter; use PHPUnit\Framework\TestCase; class ConverterTest extends TestCase { public function conversionSuccessfulProvider() { return [ [ '{"key":"value","key2":"value2"}', [ 'key' => 'value', 'key2' => 'value2', ], ], [ '{"key":"value","key2":"value2","some-array":[1,2,3,4,5]}', [ 'key' => 'value', 'key2' => 'value2', 'some-array' => [1, 2, 3, 4, 5], ], ], [ '{"key":"value","key2":"value2","some-array":[1,2,3,4,5],"new-object":{"key":"value","key2":"value2"}}', [ 'key' => 'value', 'key2' => 'value2', 'some-array' => [1, 2, 3, 4, 5], 'new-object' => [ 'key' => 'value', 'key2' => 'value2', ], ], ], [ '[{"key":"value","key2":"value2","some-array":[1,2,3,4,5],"new-object":{"key":"value","key2":"value2"}},{"key":"value","key2":"value2","some-array":[1,2,3,4,5],"new-object":{"key":"value","key2":"value2"}},{"key":"value","key2":"value2","some-array":[1,2,3,4,5],"new-object":{"key":"value","key2":"value2"}}]', [ [ 'key' => 'value', 'key2' => 'value2', 'some-array' => [1, 2, 3, 4, 5], 'new-object' => [ 'key' => 'value', 'key2' => 'value2', ], ], [ 'key' => 'value', 'key2' => 'value2', 'some-array' => [1, 2, 3, 4, 5], 'new-object' => [ 'key' => 'value', 'key2' => 'value2', ], ], [ 'key' => 'value', 'key2' => 'value2', 'some-array' => [1, 2, 3, 4, 5], 'new-object' => [ 'key' => 'value', 'key2' => 'value2', ], ], ], ], ]; } /** * @param $input * @param $output * @dataProvider conversionSuccessfulProvider */ public function testStringConversionSuccess($input, $output) { $converter = new \SitePoint\Converter\Converter(); $this->assertEquals($output, $converter->convertString($input)); } }
tests/SitePoint/Converter/ConverterTest.php
Сначала мы написали новый метод с именем conversionSuccessfulProvider
. Это намекает на ожидание того, что все предоставленные случаи должны возвращать положительный результат, поскольку выходные данные и входные данные совпадают. Поставщики данных возвращают массивы (чтобы тестовая функция могла автоматически выполнять перебор элементов). Каждый элемент этого массива представляет собой отдельный тестовый пример — в нашем случае каждый элемент представляет собой массив с двумя элементами: первый является входным, а второй выводится, как и прежде.
Затем мы объединили тестовые функции в одну с более общим именем, что указывает на то, что ожидается: testStringConversionSuccess
. Этот метод теста принимает два аргумента: вход и выход. Остальная логика идентична той, что была раньше. Кроме того, чтобы убедиться, что метод использует провайдер данных, мы объявляем провайдера в @dataProvider conversionSuccessfulProvider
метода с помощью @dataProvider conversionSuccessfulProvider
.
Это все, что нужно сделать — мы теперь получаем точно такой же результат.
Если мы теперь хотим добавить больше тестовых случаев, нам нужно только добавить больше пар ввода-вывода в провайдера. Не нужно придумывать новые имена методов или повторять логику. Удобно, правда?
Введение в покрытие кода
Прежде чем мы подпишем эту часть и позволим вам освоить все, что мы уже рассмотрели, давайте кратко обсудим покрытие кода.
Покрытие кода — это показатель, показывающий, насколько наш код покрыт тестами. Если в нашем классе есть два метода, но в тестах тестируется только один, то охват кода составляет не более 50% — в зависимости от того, сколько логических вилок (IF, переключателей, циклов и т. Д.) Имеют методы (каждый форк) должен быть покрыт отдельным тестом). PHPUnit имеет возможность генерировать отчеты о покрытии кода автоматически после запуска данного набора тестов.
Давайте быстро настроим это. Мы расширим phpunit.xml
, добавив phpunit.xml
<logging>
и <filter>
как элементы непосредственно внутри <phpunit>
, так же как элементы уровня 1 (если <phpunit>
имеет уровень 0 или root):
<phpunit ...> <filter> <whitelist> <directory suffix=".php">src/</directory> </whitelist> </filter> <logging> <log type="tap" target="tests/build/report.tap"/> <log type="junit" target="tests/build/report.junit.xml"/> <log type="coverage-html" target="tests/build/coverage" charset="UTF-8" yui="true" highlight="true"/> <log type="coverage-text" target="tests/build/coverage.txt"/> <log type="coverage-clover" target="tests/build/logs/clover.xml"/> </logging>
Фильтр устанавливает белый список, сообщающий PHPUnit, на какие файлы обращать внимание при тестировании. Этот переводит для всех файлов .php внутри / src, на любом уровне . Ведение журнала сообщает PHPUnit, какие отчеты нужно генерировать — различные инструменты могут читать различные отчеты, поэтому не повредит генерировать больше форматов, чем может потребоваться. В нашем случае нас действительно просто интересует HTML.
Прежде чем это сработает, нам нужно активировать XDebug, так как это расширение PHP, которое PHPUnit использует для проверки классов, через которые он проходит. Homestead Improved поставляется с инструментом phpenmod
для активации и деактивации расширений PHP на лету:
sudo phpenmod xdebug
Если вы не используете HI, следуйте инструкциям по установке XDebug, относящимся к вашему дистрибутиву ОС. Эта статья должна помочь.
Повторный запуск пакета теперь сообщит нам о сгенерированных отчетах о покрытии. Кроме того, они появятся в дереве каталогов в указанном месте.
Давайте откроем файл index.html
в браузере. Прямое перетаскивание в любой современный браузер должно работать просто отлично — не нужно виртуальных хостов или работающих серверов — это просто статический файл.
В индексном файле будет представлена сводка всех тестов. Вы можете щелкнуть по отдельным классам, чтобы увидеть их подробные отчеты о покрытии, и наведение курсора на тела метода вызовет всплывающие подсказки, которые объясняют, сколько тестируется данный метод.
Мы углубимся в охват кода в следующем посте по мере дальнейшего развития нашего инструмента.
Вывод
В этом введении PHPUnit мы рассмотрели тестовую разработку (TDD) в целом и применили ее концепции к начальной стадии нового инструмента PHP. Весь код, который мы написали, можно скачать с Github .
Мы изучили основы PHPUnit, объяснили поставщиков данных и показали охват кода. В этом посте были затронуты только некоторые основные понятия и возможности PHPUnit, и мы призываем вас продолжить изучение самостоятельно или запросить разъяснения по поводу понятий, которые вы считаете непонятными — мы хотели бы иметь возможность прояснить больше вопросов для вы.
В последующем посте мы рассмотрим некоторые промежуточные методы и продолжим разработку нашего приложения.
Пожалуйста, оставьте свои комментарии и вопросы ниже!