Подмена встроенных php-функций

Часто при написании тестов необходимо протестировать внешнюю функцию или функцию, которая встроенная в 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_metatrue. Но нам этого малого, для хорошего теста. В коде у нас несколько раз вызывается 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.

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

%d такие блоггеры, как: