Репозиторий моего семинара на WordCamp Catania 2019.
Необязательно, но вам может потребоваться установить Docker:
sudo apt-get install docker-ce docker-ce-cli containerd.io
Я предполагаю, что у вас установлен Composer. Давайте сначала установим PHPUnit:
composer require --dev phpunit/phpunit ^8.3
Пожалуйста, ознакомьтесь также с требованиями!
PHPUnit 8.3 требует как минимум PHP 7.2! Кстати, поддержка безопасности PHP 7.1 заканчивается 1 декабря 2019 года.
Подсказка : у вас не установлен Composer ? Попробуйте это!
docker run --rm -it -v $PWD:/app -u $(id -u):$(id -g) composer install
Есть как минимум две действующие платформы, которые пригодятся, если вы планируете тестировать расширения WordPress:
Давайте попробуем Brain Monkey :
composer require --dev brain/monkey:2.*`
Это автоматически установит также Mockery и Patchwork. Просто запустите composer install
, и все готово.
Создайте каталог, в котором будет размещен небольшой тестовый класс с именем WcctaTest.php :
mkdir -p tests/wccta
Отличный! Теперь давайте создадим файл конфигурации phpunit.xml в корневом каталоге.
Вы также можете решить запустить тесты с параметрами конфигурации из командной строки. Смотрите следующую часть (подсказка: «скрипты»)!
Большой! Добавьте несколько разделов в файл композитора.json :
composer test
Давайте создадим каталог, в котором будет храниться наш исходный код. Это место, куда вы поместите первый класс, который скоро протестируете.
mkdir -p src/wccta && touch src/wccta/Plugin.php
rm -f tests/wccta/WcctaTest.php && touch tests/wccta/PluginTest.php
touch wordpress-plugins-phpunit.php
Мы хотим протестировать некоторые методы класса Plugin
. Представьте себе метод is_loaded
, который в случае успеха возвращает true
. Когда вы будете готовы, выполните:
composer test
Подсказка : ваша система или версия PHP не обновлены? Вы можете просто пропустить этот шаг, но давайте попробуем что-нибудь [не такое] новое!
docker run -it --rm -v $PWD:/app -w /app php:7.3-alpine php ./vendor/bin/phpunit
Вы, вероятно, можете себе представить, что некоторые плагины будут иметь множество классов и что вы можете легко забыть протестировать все функции, которые требуют тестирования.
Итак, давайте поговорим о покрытии !
Просто добавьте специальную команду в раздел скриптов вашего композитора.json :
"coverage": "./vendor/bin/phpunit --coverage-html ./reports/php/coverage"
и фильтр для вашего phpunit.xml :
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory>./src</directory>
</whitelist>
</filter>
Теперь просто выполните composer coverage
! Это создаст каталог ./reports/php/coverage
вместе с некоторыми html-файлами. Ну не на всех компьютерах. Некоторые по-прежнему будут получать сообщения об ошибках, например:
Error: No code coverage driver is available
Давайте исправим это в нашем docker-образе. Я подготовил Dockerfile , чтобы вы могли просто выполнить:
docker build -t coverage .
И после завершения процесса сборки:
docker run -it --rm -v $PWD:/app -w /app coverage:latest php ./vendor/bin/phpunit --coverage-html ./reports/php/coverage
Теперь вы знаете Кунг-фу! Пожалуйста, откройте файл ./reports/php/coverage/index.html в своем браузере!
Давайте подключим наш класс Plugin
к плагину. Прежде чем мы перейдем к тестированию, я просто покажу вам, как объявить части вашего кода не подлежащими тестированию.
@codeCoverageIgnore
Это одна из важных доступных аннотаций. Мы вернемся к этому позже, но сначала:
Снова запустите модульные тесты с отчетом о покрытии!
Возможно, вы заметили столбец CRAP
в отчете об освещении. CRAP — это аббревиатура от «анти-паттернов риска изменений» . Он показывает, насколько рискованным может быть изменение кода в классе или методе. Вы можете снизить риск (и, следовательно, индекс) с помощью менее сложного кода и полного покрытия тестами.
Давайте начнем что-нибудь тестировать. Но что? До сих пор не написано никаких дополнительных функций, требующих тестирования.
Здесь в игру входит TDD (разработка через тестирование).
Даже если вы решите не использовать эту технику, вы должны хотя бы знать, о чем мы говорим.
Давайте сначала создадим Test CarTest
, который должен проверять, возвращает ли метод get_price
строку '€ 14.500'
. Затем создайте класс Car
и напишите метод get_price
, который удовлетворяет тесту. Не начинайте с реализации.
На этом этапе позвольте мне также представить шаблон тестирования AAA (Arrange Act Assert), который широко принят в TDD . Он описывает, как организовать тест, и очень похож на GWT (Given When then) из BDD (Разработка, основанная на поведении).
Вы можете проверить свои классы, если они выдают исключение в определенных условиях. Давайте теперь реализуем метод get_price
.
Просто создайте Registry
классов, который устанавливает смешанное значение в качестве именованного элемента во внутреннем массиве. Используйте для этого метод set()
или магический метод __set()
. Прежде всего предположим, что мы можем передать объект JSON нашему классу Car
. Это придаст нашему классу немного большую ценность.
Другой метод get
или __get()
должен проверять, существует ли элемент с заданным значением, и возвращать его в случае успеха. Если такого элемента нет, создайте исключение OutOfBoundsException
. Теперь напишите конструктор, который обрабатывает входные данные JSON и сохраняет объект в data
member-var. Метод get_price
должен брать цену из переменной data
и обрабатывать форматированный вывод.
Проверьте ветку шага 10, если вам сложно писать код! Переменная price
должна быть целым числом. Вероятно, сейчас это не проблема, потому что вы можете использовать функцию PHP number_format()
для создания правильного вывода. Но при установке WordPress вы ожидаете, что будет установлен языковой стандарт, например, it_IT
(итальянский).
Правильный способ форматирования чисел в WordPress — использование функции number_format_i18n()
.
Итак, давайте изменим это и посмотрим, что произойдет:
Error: Call to undefined function wcctanumber_format_i18n()
Мы исправим это через секунду, но сначала давайте немного подготовимся. Brain Monkey использует setUp()
и tearDown()
предоставляемые PHPUnit . Вы можете переопределить эти методы. Давайте создадим собственный TestCase
— назовем его WcctaCase
— который мы сможем расширить, потому что мы, вероятно, будем делать это в каждом тестовом классе.
Теперь добавим пространство имен для тестов в секцию autoload-dev:
"autoload-dev": {
"psr-4": {
"tests\wccta\": "tests/wccta"
}
},
Наконец, давайте изменим родителя наших тестовых классов.
class CarTest extends WcctaTestCase { // ... }
Мы готовы имитировать нашу первую функцию WordPress с помощью
Functionsexpect( $name_of_function )->andReturn( $value );
Написание теста только для одного ожидания кажется слишком трудным. Что делать, если вы хотите протестировать разные значения?
Поставщик данных спешит на помощь. Я уже говорил об аннотациях на шаге 5. Это тоже очень полезно:
@dataprovider method_that_returns_data
Посмотрите на мой пример. getData
возвращает массив массивов. Каждый из этих массивов содержит 3 значения. Наш метод test_getPrice
может не только принимать поставщика данных с аннотацией, но также определять входные переменные в качестве параметров.
Вы можете проверить свои классы, если они выдают исключение в определенных условиях.
Просто создайте Registry
классов, который устанавливает смешанное значение в качестве именованного элемента во внутреннем массиве. Используйте для этого метод set()
или магический метод __set()
.
Другой метод get
или __get()
должен проверять, существует ли элемент с заданным ключом, и возвращать его в случае успеха. Если такого элемента нет, создайте исключение OutOfBoundsException
.
Проверьте ветку шага 10, если вам сложно писать код!
Последние шаги привели нас к Фабрикам . Что такое фабрика? Иногда вы создаете функции или методы, которые просто скрывают сложный процесс создания конкретного объекта. И иногда вам нужно решить, какой тип объекта вы хотите создать.
В плагинах WordPress я предпочитаю добавлять хуки в фабриках к объектам. Существуют плагины, которые добавляют хуки в конструкторы классов. Это не очень хорошо (особенно если вы все еще тестируете классический способ — создание полноценной среды с работающим WordPress).
Давайте создадим класс Factory
со статической функцией с именем create
. Этот метод должен возвращать объект Car
. Но давайте проведем рефакторинг конструктора Car
так, чтобы он уже ожидал объект, а не JSON-строку. Вместо этого мы сделаем это в методе create класса Factory
.
Проверьте свой плагин сейчас с помощью composer test
, и вы увидите некоторые ошибки:
TypeError: Argument 1 passed to wcctaCar::__construct() must be an object, string given, called in ...
Нам тоже следует подкорректировать наши тесты...
Отличный! Давайте создадим тест для нашей Фабрики. На данный момент мы оставим метод без какого-либо содержания. Проведите тесты еще раз!
There was 1 risky test:
1) testswcctaFactoryTest::test_create
This test did not perform any assertions
Тесты проходят успешно, но вы получаете сообщение о том, что был рискованный тест. Кстати: назовите функцию test_create
просто create
и используйте аннотацию @test
. Я считаю, что использование этой аннотации зависит от вашего личного вкуса!
Теперь мы углубимся в это немного глубже.
Создайте интерфейс FooterInterface
, который определяет info
об общедоступном методе, который не ожидает никакого возвращаемого значения. Реализуйте интерфейс в Car
, info
мог бы, например, выводить забавное сообщение.
Определите тип возвращаемого значения FooterInterface
для метода create
Factory
и добавьте метод info
Car
в действие WordPress wp_footer
.
Теперь давайте проверим это в FactoryTest
. Есть как минимум два способа проверить это правильно. Используйте has_action или ActionsexpectAdded()
. Тест фильтров будет аналогичным и хорошо описан на связанной странице.
Проверьте, проходит ли composer test
все тесты.
Как сейчас обстоят дела с покрытием? Выполните composer coverage
и проверьте сгенерированный вывод.
info
метод нашего класса Car
не охвачен никаким тестом. Но можем ли мы проверить выходные данные метода?
Оказывается, с ожиданиемOutputString это довольно просто.
Давайте отпразднуем то, что мы узнали!
Создайте класс Locale
с общедоступным методом get
, который возвращает get_locale()
. Исключите метод из покрытия!
Теперь создайте конструктор в нашем классе Plugin
, который принимает экземпляр Locale
, и сохраните его в переменной-члене $this->locale
. Затем создайте метод get_region_code
, который возвращает значение $this->locale->get()
. Ах, и удалите метод is_loaded
. ;)
В нашем тесте мы могли создать объект типа Locale
, имитировать функцию WordPress get_locale
и передать ее конструктору Plugin
! Но я хочу, чтобы здесь был Мокер:
public function test_get_region_code() {
$code = 'it_IT';
$locale = Mockery::mock( Locale::class );
$locale->shouldReceive( 'get' )->andReturn( $code );
$sut = new Plugin( $locale );
$this->assertEquals( $code, $sut->get_region_code() );
}
Теперь вы можете сделать свои плагины WordPress пуленепробиваемыми!
Веселиться!