Кто еще не знаком с тестированием и модульным тестированием можете ознакомится: Автоматизация тестирования, Модульное тестирование с помощью PHPUnit.
Тестирование тем и плагинов под WordPress имеет одну большую проблему — взаимодействие с ядром. Решить ее можно с помощью библиотеки 10up/WP_Mock.
Библиотека 10up/WP_Mock помогает делать заглушки для ф-ций и классов из ядра WordPress.
Установка библиотеки 10up/WP_Mock для тестирования WordPress
Устанавливаем библиотеку через composer:
composer require --dev 10up/wp_mock
Настройка файла конфигурации phpunit.xml
Создаем файл конфига /tests/phpunit/phpunit.xml:
<phpunit
bootstrap="./bootstrap.php"
backupGlobals="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
beStrictAboutTestsThatDoNotTestAnything="false"
>
<testsuites>
<testsuite name="Config-example-for-tests">
<directory suffix=".php">./tests/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory suffix=".php">../../</directory>
<exclude>
<file>../../my-plugin.php</file>
<directory>../../tests</directory>
<directory>../../vendor</directory>
<directory>../../assets</directory>
</exclude>
</whitelist>
</filter>
</phpunit>
Важными являются настройки:
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
beStrictAboutTestsThatDoNotTestAnything="false"
Эти настройки позволяют преобразовать все errors, notices, warnings в exception, а be Strict About Tests That Do Not Test Anything разрешает тестам не возвращать assert в конце.
Создание bootstrap.php
Создаем /tests/phpunit/bootsrap.php:
<?php
require_once __DIR__ . '/../../vendor/autoload.php';
WP_Mock::bootstrap();
WP_Mock::bootstrap() запускает библиотеку WP_Mock.
В тестовых классах нужно использовать фикстуры setUp, tearDown:
class Test_Main extends PHPUnitFrameworkTestCase {
public function setUp(): void {
parent::setUp();
WP_Mock::setUp();
}
public function tearDown(): void {
WP_Mock::tearDown();
Mockery::close();
parent::tearDown();
}
}
Теперь мы можем делать заглушки абсолютно для любых функций и классов WordPress.
Первый тест с помощью WP_Mock
Пример 1. Тестируем добавление метаполя при сохранении поста:
public function save( int $post_id ): void {
$nonce = filter_input( INPUT_POST, $this->plugin_slug . '_nonce', FILTER_SANITIZE_STRING );
if ( ! wp_verify_nonce( $nonce, $this->plugin_slug ) ) {
return;
}
$field = filter_input( INPUT_POST, 'field', FILTER_SANITIZE_STRING );
update_post_meta( $post_id, 'field', $field );
}
public function test_no_save(): void {
WP_Mock::userFunction(
'wp_verify_nonce',
[
'times' => 1,
'return' => false,
]
);
$metabox = new My_PluginMetabox();
$metabox->save( 10 );
}
public function test_save(): void {
$post_id = 10;
WP_Mock::userFunction(
'wp_verify_nonce',
[
'times' => 1,
'return' => true,
]
);
WP_Mock::userFunction(
'update_post_meta',
[
'args' => [
$post_id,
'field',
'*',
],
'times' => 1,
]
);
$metabox = new My_PluginMetabox();
$metabox->save( $post_id );
}
Таким образом, если в методе save кто-то поменяет поле для nonce или уберет проверку, то тесты буду провалены. Так же мы проверили с какими данными должно обновляться метаполе. Первый параметр — этот тот же $post_id, который приходит первым аргументом на хук save_post. Жестко указываем, как называется метаполе ‘field’, не важно, какие данные могут приходить в последний параметр ( ‘*’ ). И вызваться должно все по 1 разу.
Заглушки для всех WordPress функций (WP_Mock::userFunctions)
Для этого служит статический метод WP_Mock::userFunction, который мы уже использовали в примере выше. Это один из основных инструментов для модульного тестирования для WordPress. После его освоение можно будет легко тестировать большую часть функций и методов, написаные с использованием WordPress.
Теперь рассмотрим данный метод более подробнее. Первый параметр это название функции, которую мы хотим подменить. Второй параметр это массив аргументов. На данный момент возможны такие аргументы: times, args, return и return_in_order.
Аргумент times
Аргумент times — означает, что функция будет вызвана столько раз, сколько указано в агрументе. Примеры:
WP_Mock::userFunction( 'get_post_meta' ); // Функция вызывается 0 или более раз.
WP_Mock::userFunction( 'get_post_meta', [ 'times' => 1 ] ); // функция должна быть вызвана только 1 раз.
WP_Mock::userFunction( 'get_post_meta', [ 'times' => '1+' ] ); // функция вызывается 1 или более раз.
WP_Mock::userFunction( 'get_post_meta', [ 'times' => '1-3' ] ); // функция вызывается от 1 до 3 раз.
WP_Mock::userFunction( 'get_post_meta', [ 'times' => '3-' ] ); // функция вызывается не более 3 раз.
Аргумент args
Аргумент args — означает, что функция должна быть вызвана с такими параметрами. Примеры:
WP_Mock::userFunction( 'get_post_meta ); // Функция вызывается с любыми параметрами.
WP_Mock::userFunction(
'get_post_meta',
[
'args' => [
10,
'some-field',
true,
],
]
); // функция вызывается с агрументами ( 10, 'some-field', true );
WP_Mock::userFunction(
'get_post_meta',
[
'args' => [
'*',
WP_MockFunctions::type( 'int' ),
WP_MockFunctions::anyOf( [ true, false ] ),
],
]
);
/**
* '*' - означает, что аргумент может быть любой;
* WP_MockFunctions::type( 'int' ) - аргумент должен быть любой с типом данных int. Возможны любые из типов данных php (int, string, callable ... );
* WP_MockFunctions::anyOf( [ true, false ] ) - аргумент доллжен быть любой из текущего массива;
*/
Аргумент return
Аргумент return — результат функции будет тот, который передается в данный аргумент. Примеры:
WP_Mock::userFunction( 'get_post_meta', [ 'return' => true ] ); // функция вернет true;
WP_Mock::userFunction( 'get_post_meta', [ 'return' => 'some-string' ] ); // функция вернет 'some-string';
WP_Mock::userFunction( 'get_post_meta', [ 'return' => new stdClass() ] ); // функция вернет объект;
WP_Mock::userFunction( 'get_post_meta', [ 'return' => Mockery::mock( 'ExampleNamespaceObject' ) ] ); // функция вернет заглушку ExampleNamespaceObject;
Аргумент return_in_order
Аргумент return_in_order — результат функции будет при каждом вызове в порядке переданные в массиве. Пример:
WP_Mock::userFunction(
'is_single',
[
'return_in_order' => [ true, false, 'string' ]
]
);
is_single() // true;
is_single() // false;
is_single() // 'string';
Тестирование хуков
Для do_action, add_action, apply_filters, add_filter используются свои способы тестирования.
Тестирование добавление хуков add_action, add_filter (expectActionAdded, expectFilterAdded)
Добавление хуков тестируется с помощью методов expectActionAdded и expectFilterAdded. Мы ожидаем, что тестируемый метод/функция будет подключать свои хуки. Пример:
function suit() {
add_action( 'save_post', 'special_save_post', 10, 2 );
add_filter( 'the_content', 'special_the_content' );
}
...
public function test_suit() {
WP_Mock::expectActionAdded( 'save_post', 'special_save_post', 10, 2 );
WP_Mock::expectFilterAdded( 'the_content', 'special_the_content' );
suit();
}
...
Тестирование наличия хуков do_action, apply_filters (expectAction, expectFilter onFilter)
Наличие хуков тестируется с помощью методов expectAction, expectFilter и onFilter. Мы ожидаем, что тестируемый метод/функция будет использовать свои хуки. Пример:
function suit() {
do_action( 'my_action' );
return apply_filters( 'my_filter', 'suit text' );
}
...
public function test_suit() {
WP_Mock::expectAction( 'my_action' );
WP_Mock:: expectFilter( 'my_filter' );
$result = suit();
$this->assertSame( 'suit text', $result );
}
...
public function test_suit_with_on_filtered() {
WP_Mock::expectAction( 'my_action' );
WP_Mock::onFilter( 'my_filter' )
->with( 'suit text' )
->reply( 'suit was filtered' );
$result = suit();
$this->assertSame( 'suit was filtered', $result );
}
С помощью onFilter можно более детально протестировать наличие фильтров.
Тестирование глобальных переменных
Да-да WordPress грешит такими вещами, как глобальные переменные. Разберем работу с базой. Самый распространенный пример с использованием глобальных переменных это работа БД.
Пример кода, аналог функции get_option():
function example_get_option( string $name ) {
global $wpdb;
return $wpdb->get_var( 'SELECT option_value FROM ' . $wpdb->options . ' WHERE option_name = "' . $name . '"' );
}
Тестирование примера:
$test_case = 'example_option_name';
global $wpdb;
$wpdb = Mockery::mock( 'wpdb' ); // Создаем заглушку для wpdb;
$wpdb->options = 'wp_options'; // Определяем нужные свойства для заглушки
$wpdb->shouldReceive( 'get_var' ) // Ожидаем, что тестируемая функция будет вызывать $wpdb->get_var();
->once() // Один раз;
->with( 'SELECT option_value FROM wp_options WHEN option_name = "' . $test_case . '"' ) // С такими аргументами;
->andReturn( true ); // И вернет true;
$result = example_get_option( $test_case ); // Вызов функции
$this->assertTrue( $result ); // Утверждаем, что функция вернула true;
В целом данных возможностей достаточно, чтобы начать тестировать свой код на WordPress.