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

34

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

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

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

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

Благодарность!

Файл начальной загрузки выглядит так:

$_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' ) );
  }
}

Благодарность!

Ионут Стайку
источник
Если вы работаете phpunit, видите ли вы неудачные или пройденные тесты? Вы установили bin/install-wp-tests.sh?
Свен
Я думаю, что часть проблемы в том, что, возможно RegisterCustomPostType::__construct(), никогда не вызывается, когда плагин загружается для тестов. Также возможно, что на вас повлияла ошибка # 29827 ; возможно, попробуйте обновить вашу версию пакета модульных тестов WP.
JD
@Sven: да, тесты не пройдены; я установил bin/install-wp-tests.sh(так как я использовал WP-АОН) @JD: RegisterCustomPostType :: __ конструкция будет называется (просто добавил die()заявление и PHPUnit останавливается там)
Йонут Staicu
Я не слишком уверен в модульном тестировании (не моя сильная сторона), но с буквальной точки зрения вы можете использовать, did_action()чтобы проверить, сработали ли действия.
Rarst
@Rarst: спасибо за предложение, но оно все еще не работает. По какой-то причине, я думаю, что выбор времени неправильный (тесты запускаются до initхука).
Ionut Staicu

Ответы:

72

Тест в изоляции

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

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

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

Изолятор

Это причина, почему модульные тесты называются «модульными».

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

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

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

Давайте посмотрим тестовый код в 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, то вы проверяете поведение WordPress, а не поведение вашего плагина ...

Хорошо .. но это ПИТА!

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

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

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

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

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

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

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

Это позволяет вам смоделировать WP функции хуки . Предполагая, что вы загрузили в свои тесты (см. Репозиторий 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 есть встроенная система для имитации объектов, но здесь я хочу предложить вам Mockery . Это очень мощная библиотека и очень проста в использовании. Более того, это зависимость WP_Mock, поэтому, если она у вас есть, у вас тоже есть Mockery.

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

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

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

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

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

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

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

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

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

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

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

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

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

gmazzap
источник
5
Святое дерьмо, много полезной информации! Спасибо! Каким-то образом мне удалось пропустить весь смысл модульного тестирования (до сих пор я практиковал тестирование PHP только внутри Code Dojo). Я также узнал о wp_mock ранее сегодня, но по какой-то причине мне удается игнорировать его. Меня бесило то, что любой тест, каким бы маленьким он ни был, запускался не менее двух секунд (сначала загрузите WP env, затем выполните тест). Еще раз спасибо за то, что открыли мне глаза!
Ionut Staicu
4
Спасибо @IonutStaicu, я забыл упомянуть, что не загрузка WordPress делает ваши тесты намного быстрее
gmazzap
6
Также стоит отметить, что инфраструктура модульных тестов WP Core является замечательным инструментом для запуска тестов INTEGRATION, которые будут автоматизированными тестами, обеспечивающими его хорошую интеграцию с самим WP (например, нет случайных коллизий имен функций и т. Д.).
Джон П Блох
1
@JohnPBloch +1 за хорошую точку. Даже если использования пространства имен достаточно, чтобы избежать столкновения имен функций в WordPress, где все является глобальным :) Но, конечно, интеграция / функциональные тесты - это вещь. Сейчас я играю с Behat + Mink, но все еще практикуюсь с этим.
gmazzap
1
Спасибо за "
полет