Модульное тестирование WordPress (PHPUnit, WP_Mock)

8 мин. 5 сек.
365
7

Кто еще не знаком с тестированием и модульным тестированием можете ознакомится: Автоматизация тестирования, Модульное тестирование с помощью 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 \PHPUnit\Framework\TestCase {

	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_Plugin\Metabox();
	$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_Plugin\Metabox();
	$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_Mock\Functions::type( 'int' ),
			WP_Mock\Functions::anyOf( [ true, false ] ),
		],
	]
);
/**
 * '*' - означает, что аргумент может быть любой;
 * WP_Mock\Functions::type( 'int' ) - аргумент должен быть любой с типом данных int. Возможны любые из типов данных php (int, string, callable ... );
 * WP_Mock\Functions::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( '\Example\Namespace\Object' ) ] ); // функция вернет заглушку \Example\Namespace\Object;

Аргумент 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.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *