Часто при написании тестов необходимо протестировать внешнюю функцию или функцию, которая встроенная в php. Рассмотрим для примера сохранение метаполей для постов в WordPress:
class Metabox {
public function save( int $post_id ) {
$nonce = filter_input( INPUT_POST, '_nonce', FILTER_SANITIZE_STRING );
if ( ! wp_verify_nonce( $nonce, 'very-secret-nonce' ) ) {
return;
}
$field = filter_input( INPUT_POST, 'field', FILTER_SANITIZE_STRING );
update_post_meta( $post_id, 'field', $field );
}
}
$metabox = new Metabox();
add_action( 'save_post', [ $metabox, 'save' ] );
В данном примере мы имеем внешние ф-ции: wp_verify_nonce
, update_post_meta
; и встроенную ф-цию php — filter_input
.
Если с внешними функциями мы можем справится с помощью WP_Mock. То со встроенной функцией filter_input
все немного сложнее т.к. она уже встроенная в php и заменить ее с помощью WP_Mock
не получится. Но с этой проблемой легко справится Function Mocker.
Если запустить юнит-тест и посмотреть, что будет в переменных $nonce и $field, то они всегда будут null.
Установка Function Mocker
composer require lucatume/function-mocker:~1.0
Function Mocker в bootstrap.php
Теперь нужно подключить библиотеку в файле bootstrap.php:
use tadFunctionMockerFunctionMocker;
// ...
FunctionMocker::init(
[
'whitelist' => [
realpath( PLUGIN_PATH . '/src/ ),
],
'blacklist' => [
realpath( PLUGIN_PATH ),
],
'redefinable-internals' => [ 'filter_input' ],
]
);
Нужно инициализровать библиотеку с помощью FunctionMocker::init и передать параметры:
- whitelist — путь к папке, где лежат файлы проекта, которые вы будете тестировать;
- blacklist — путь к файлам, где запрещается подмена функций;
- redefinable-internals — массив функций, которые нужно переопределить.
Фикстуры для Function Mocker
Последним подготовительным этапом для начала тестирования это добавление фикстур:
<?php
use PHPUnitFrameworkTestCase;
use tadFunctionMockerFunctionMocker;
class Test_Metabox extends TestCase {
public function setUp() {
FunctionMocker::setUp();
parent::setUp();
}
public function tearDown() {
parent::tearDown();
FunctionMocker::tearDown();
}
}
Пример теста
Теперь начинаем писать сам тест:
<?php
use PHPUnitFrameworkTestCase;
use tadFunctionMockerFunctionMocker;
class Test_Metabox extends TestCase {
public function setUp() {
FunctionMocker::setUp();
parent::setUp();
}
public function tearDown() {
parent::tearDown();
FunctionMocker::tearDown();
}
public function test_save() {
$nonce = 'nonce';
FunctionMocker::replace( 'filter_input', $nonce );
FunctionMocker::replace( 'wp_verify_nonce', true );
FunctionMocker::replace( 'update_post_meta', true );
$metabox = new Metabox();
$metabox->save( 10 );
}
}
Теперь все вызовы в тестовом методе filter_input
возвращают строку nonce
, а вызовы wp_verify_nonce
и update_post_meta
— true
. Но нам этого малого, для хорошего теста. В коде у нас несколько раз вызывается filter_input и нам необходимо получить разные ответы:
<?php
use PHPUnitFrameworkTestCase;
use tadFunctionMockerFunctionMocker;
class Test_Metabox extends TestCase {
public function test_save() {
$nonce = 'nonce';
$field = 'some-text-field';
FunctionMocker::replace(
'filter_input',
function () use ( $nonce, $field ) {
static $i = 0;
$answers = [ $nonce, $field ];
return $answers[ $i ++ ];
}
);
// Or in latest version
// FunctionMocker::replaceInOrder( 'filter_input', [ $nonce, $field ] );
FunctionMocker::replace( 'wp_verify_nonce', true );
FunctionMocker::replace( 'update_post_meta', true );
$metabox = new Metabox();
$metabox->save( 10 );
}
}
Теперь, с помощью анонимной функции возвращаем при первом вызове строку nonce
, а при втором some-text-field
. В более поздних версиях можно это сделать более кратко с помощью FunctionMocker::replaceInOrder
.
Осталось проверить, какие параметры мы передаем в функции:
<?php
use PHPUnitFrameworkTestCase;
use tadFunctionMockerFunctionMocker;
class Test_Metabox extends TestCase {
public function setUp() {
FunctionMocker::setUp();
parent::setUp();
}
public function tearDown() {
parent::tearDown();
FunctionMocker::tearDown();
}
public function test_save() {
$post_id = 10;
$nonce = 'nonce';
$field = 'some-text-field';
$filter_input = FunctionMocker::replace(
'filter_input',
function () use ( $nonce, $field ) {
static $i = 0;
$answers = [ $nonce, $field ];
return $answers[ $i ++ ];
}
);
$wp_verify_nonce = FunctionMocker::replace( 'wp_verify_nonce', true );
$update_post_meta = FunctionMocker::replace( 'update_post_meta', true );
$metabox = new Metabox();
$metabox->save( $post_id );
$filter_input->wasCalledWithOnce( [ INPUT_POST, '_nonce', FILTER_SANITIZE_STRING ] );
$filter_input->wasCalledWithOnce( [ INPUT_POST, 'field', FILTER_SANITIZE_STRING ] );
$wp_verify_nonce->wasCalledWithOnce( [ $nonce, 'very-secret-nonce' ] );
$update_post_meta->wasCalledWithOnce( [ $post_id, 'field', $field ] );
}
}
Если вы сделали все правильно, то вы получили успешный тест 🙂 и опыт для тестирования внешних функций и встроенных в php.