Тестирование перехватов обратного вызова


Я разрабатываю плагин с использованием TDD, и есть одна вещь, которую я совершенно не могу протестировать... крючки.

Я имею в виду, хорошо, я могу проверить обратный вызов крючка, но как я могу проверить, действительно ли срабатывает крючок (как пользовательские крючки, так и крючки WordPress по умолчанию)? Я предполагаю, что некоторые насмешки помогут, но я просто не могу понять, чего мне не хватает.

Я установил набор тестов с помощью WP-CLI. Согласно этот ответ, init крючок еще должен сработать... это не так; кроме того, код работает внутри WordPress.

Насколько я понимаю, начальная загрузка загружается последней, поэтому имеет смысл не запускать инициализацию, поэтому остается вопрос: как, черт возьми, я должен проверить, срабатывают ли крючки?

Спасибо!

Файл начальной загрузки выглядит следующим образом:

$_tests_dir = getenv('WP_TESTS_DIR');
if ( !$_tests_dir ) $_tests_dir = '/tmp/wordpress-tests-lib';

require_once $_tests_dir . '/includes/functions.php';

function _manually_load_plugin() {
  require dirname( __FILE__ ) . '/../includes/RegisterCustomPostType.php';
}
tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' );

require $_tests_dir . '/includes/bootstrap.php';

Тестируемый файл выглядит следующим образом:

class RegisterCustomPostType {
  function __construct()
  {
    add_action( 'init', array( $this, 'register_post_type' ) );
  }

  public function register_post_type()
  {
    register_post_type( 'foo' );
  }
}

И сам тест:

class CustomPostTypes extends WP_UnitTestCase {
  function test_custom_post_type_creation()
  {
    $this->assertTrue( post_type_exists( 'foo' ) );
  }
}

Спасибо!

Author: Community, 2014-10-11

1 answers

Испытание в изоляции

При разработке плагина лучшим способом его тестирования является без загрузки среды WordPress.

Если вы пишете код, который можно легко протестировать без WordPress, ваш код становится лучше.

Каждый компонент, прошедший модульное тестирование, должен быть протестирован в изоляции: при тестировании класса вам нужно протестировать только этот конкретный класс, предполагая, что весь остальной код работает идеально.

The Isolator

Именно по этой причине модульные тесты называются "единичными".

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

Избегайте зацепок в конструкторе

Совет, который я могу вам дать, состоит в том, чтобы избегать использования крючков в конструкторах. Это одна из вещей, которая сделает ваш код тестируемым в изоляции.

Давайте посмотрим тестовый код в OP:

class CustomPostTypes extends WP_UnitTestCase {
  function test_custom_post_type_creation() {
    $this->assertTrue( post_type_exists( 'foo' ) );
  }
}

И давайте предположим, что этот тест не проходит . Кто виновник?

  • крючок не был добавлен вообще или неправильно?
  • метод, регистрирующий тип сообщения, вообще не вызывался или с неправильными аргументами?
  • в WordPress есть ошибка?

Как это можно улучшить?

Давайте предположим, что ваш код класса:

class RegisterCustomPostType {

  function init() {
    add_action( 'init', array( $this, 'register_post_type' ) );
  }

  public function register_post_type() {
    register_post_type( 'foo' );
  }
}

( Примечание: Я буду ссылаться на эту версию класса для остальной части ответа)

Способ, которым я написал этот класс, позволяет создавать экземпляры класс без звонка add_action.

В классе выше есть 2 вещи, которые необходимо проверить:

  • метод init на самом деле вызывает add_action, передавая ему соответствующие аргументы
  • метод register_post_type на самом деле вызывает register_post_type функцию

Я не говорил, что вы должны проверять, существует ли тип записи: если вы добавите правильное действие и вызовете register_post_type, пользовательский тип записи должен существовать: если он не существует, это WordPress проблема.

Помните: когда вы тестируете свой плагин, вы должны протестировать свой код, а не код WordPress. В своих тестах вы должны исходить из того, что WordPress (как и любая другая внешняя библиотека, которую вы используете) работает хорошо. В этом смысл единичного теста.

Но... на практике?

Если WordPress не загружен, если вы попытаетесь вызвать методы класса выше, вы получите фатальную ошибку, поэтому вам нужно издеваться над функциями.

"Ручной" метод

Конечно, ты можешь напишите свою издевательскую библиотеку или "вручную" издевайтесь над каждым методом. Это возможно. Я расскажу вам, как это сделать, но затем я покажу вам более простой метод.

Если WordPress не загружается во время выполнения тестов, это означает, что вы можете переопределить его функции, напримерadd_action или register_post_type.

Предположим, у вас есть файл, загруженный из вашего загрузочного файла, в котором у вас есть:

function add_action() {
  global $counter;
  if ( ! isset($counter['add_action']) ) {
    $counter['add_action'] = array();
  }
  $counter['add_action'][] = func_get_args();
}

function register_post_type() {
  global $counter;
  if ( ! isset($counter['register_post_type']) ) {
    $counter['register_post_type'] = array();
  }
  $counter['register_post_type'][] = func_get_args();
}

Я переписал функции, чтобы просто добавлять элемент в глобальный массив каждый раз, когда они вызываются.

Теперь ты следует создать (если у вас его еще нет) свой собственный базовый класс тестового случая, расширяющий PHPUnit_Framework_TestCase: это позволяет легко настраивать ваши тесты.

Это может быть что-то вроде:

class Custom_TestCase extends \PHPUnit_Framework_TestCase {

    public function setUp() {
        $GLOBALS['counter'] = array();
    }

}

Таким образом, перед каждым тестом глобальный счетчик сбрасывается.

А теперь ваш тестовый код (я ссылаюсь на переписанный класс, который я опубликовал выше):

class CustomPostTypes extends Custom_TestCase {

  function test_init() {
     global $counter;
     $r = new RegisterCustomPostType;
     $r->init();
     $this->assertSame(
       $counter['add_action'][0],
       array( 'init', array( $r, 'register_post_type' ) )
     );
  }

  function test_register_post_type() {
     global $counter;
     $r = new RegisterCustomPostType;
     $r->register_post_type();
     $this->assertSame( $counter['register_post_type'][0], array( 'foo' ) );
  }

}

Вы должны отметить:

  • Я смог вызвать два метода отдельно, и WordPress вообще не загружается. Этот таким образом, если один тест провалится, я точно знаю , кто виноват.
  • Как я уже сказал, здесь я проверяю, что классы вызывают функции WP с ожидаемыми аргументами. Нет необходимости проверять, действительно ли CPT существует. Если вы проверяете существование CPT, то вы тестируете поведение WordPress, а не поведение вашего плагина...

Мило.. но это же ПИТА!

Да, если вам придется вручную издеваться над всеми функциями WordPress, это действительно больно. Несколько общих советов, которые я могу дать, это используйте как можно меньше функций WP: вам не нужно переписывать WordPress, но абстрактные функции WP, которые вы используете в пользовательских классах, чтобы их можно было издеваться и легко тестировать.

Например, в приведенном выше примере вы можете написать класс, который регистрирует типы сообщений, вызывая register_post_type при "инициализации" с заданными аргументами. С помощью этой абстракции вам все равно нужно протестировать этот класс, но в других местах вашего кода, которые регистрируют типы записей, вы можете использовать этот класс, издеваясь над ним в тесты (при условии, что это работает).

Самое удивительное, что если вы напишете класс, который абстрагирует регистрацию CPT, вы можете создать для него отдельный репозиторий, и благодаря современным инструментам, таким как Composer, внедрите его во все проекты, где вам это нужно: протестируйте один раз, используйте везде. И если вы когда-нибудь обнаружите в нем ошибку, вы можете исправить ее в одном месте и с помощью простого composer update все проекты, в которых она используется, тоже исправлены.

Во второй раз: написать код, который тестируемость в изоляции означает написание лучшего кода.

Но рано или поздно мне нужно будет где-то использовать функции WP...

Конечно. Вы никогда не должны действовать параллельно ядру, это не имеет смысла. Вы можете писать классы, которые обертывают функции WP, но эти классы тоже необходимо протестировать. "Ручной" метод, описанный выше, может использоваться для очень простых задач, но когда класс содержит много функций WP, это может быть проблемой.

К счастью, там есть хорошие люди которые пишут хорошие вещи. 10уп, одно из крупнейших агентств WP, поддерживает очень большую библиотеку для людей, которые хотят правильно тестировать плагины. Это так WP_Mock.

Это позволяет вам имитировать функции WP с помощью крючков. Предполагая, что вы загрузили в свои тесты (см. repo readme) тот же тест, который я написал выше, становится:

class CustomPostTypes extends Custom_TestCase {

  function test_init() {
     $r = new RegisterCustomPostType;
     // tests that the action was added with given arguments
     \WP_Mock::expectActionAdded( 'init', array( $r, 'register_post_type' ) );
     $r->init();
  }

  function test_register_post_type() {
     // tests that the function was called with given arguments and run once
     \WP_Mock::wpFunction( 'register_post_type', array(
        'times' => 1,
        'args' => array( 'foo' ),
     ) );
     $r = new RegisterCustomPostType;
     $r->register_post_type();
  }

}

Просто, не так ли? Этот ответ не является учебным пособием для WP_Mock, поэтому прочитайте readme репо для получения дополнительной информации, но пример выше должно быть довольно ясно, я думаю.

Более того, вам не нужно самостоятельно писать какие-либо издевательские add_action или register_post_type или поддерживать какие-либо глобальные переменные.

И классы WP?

В WP тоже есть некоторые классы, и если WordPress не загружается при запуске тестов, вам нужно издеваться над ними.

Это намного проще, чем издеваться над функциями, PHPUnit имеет встроенную систему для издевательства над объектами, но здесь я хочу предложить вам Издевательство. Это очень мощная библиотека и очень прост в использовании. Более того, это зависимость WP_Mock, так что, если она у вас есть, у вас тоже есть Издевательство.

Но как насчет WP_UnitTestCase?

Набор тестов WordPress был создан для тестирования ядра WordPress , и если вы хотите внести свой вклад в ядро, это имеет решающее значение, но использование его только для плагинов заставляет вас тестировать не изолированно.

Взгляните на мир WP: существует множество современных фреймворков PHP и CMS, и ни один из них не предлагает тестировать плагины/модули/расширения (или как бы они ни назывались) с использованием кода фреймворка.

Если вы пропустите фабрики, полезную функцию пакета, вы должны знать, что там есть потрясающие вещи.

Недостатки и недостатки

Есть случай, когда в рабочем процессе, который я предложил здесь, отсутствует: тестирование пользовательской базы данных.

На самом деле, если вы используете стандартные таблицы и функции WordPress для записи там (на самом низком уровне методов $wpdb), вам никогда не понадобится на самом деле писать данные или проверьте, есть ли данные на самом деле в базе данных, просто убедитесь, что правильные методы вызываются с правильными аргументами.

Однако вы можете писать плагины с пользовательскими таблицами и функциями, которые создают запросы для записи туда, и проверять, работают ли эти запросы, это ваша ответственность.

В этих случаях набор тестов WordPress может вам очень помочь, и в некоторых случаях может потребоваться загрузка WordPress для запуска таких функций, как dbDelta.

(Нет необходимости говорить, чтобы использовать другой база данных для тестов, не так ли?)

К счастью, PHPUnit позволяет вам организовывать ваши тесты в "наборы", которые можно запускать отдельно, поэтому вы можете написать набор для пользовательских тестов базы данных, в котором вы загружаете среду WordPress (или ее часть), оставляя все остальные ваши тесты без WordPress.

Только обязательно пишите классы, которые абстрагируют как можно больше операций с базой данных, таким образом, чтобы все остальные классы плагинов использовали их, чтобы с помощью mocks вы могли правильно протестировать большинство классов не имеют дела с базой данных.

В третий раз написание кода, легко тестируемого в изоляции, означает написание лучшего кода.

 83
Author: gmazzap, 2020-06-15 08:21:38