Перейти к содержимому

Интеграционные тесты: Pest + WP-CLI

Пока плагин маленький, хватает ручной проверки. Но когда появляется бизнес-логика, синхронизация данных, WooCommerce, внешние API и несколько режимов работы — ручное тестирование не справляется.

Стек Pest PHP + WP-CLI + wp-env даёт:

  • Тесты в живом WordPress-окружении (не моки)
  • Лаконичный синтаксис (как в Laravel)
  • Воспроизводимость (одна команда — полный прогон)
  • Изоляцию тестов через транзакции
ИнструментРоль
@wordpress/envDocker-окружение с WordPress
DockerКонтейнеры для WP, PHP и БД
ComposerPHP-зависимости
Pest PHPЛаконичный синтаксис тестов поверх PHPUnit
WP-CLIЕдиная точка входа для запуска тестов
MakefileКороткие команды

Минимальный конфиг .wp-env.json:

{
"core": null,
"phpVersion": "8.3",
"plugins": [ ".", "https://downloads.wordpress.org/plugin/woocommerce.zip" ],
"port": 8888,
"config": {
"WP_DEBUG": true,
"WP_DEBUG_LOG": true,
"WP_DEBUG_DISPLAY": false,
"SCRIPT_DEBUG": true
}
}
  • "core": null — последняя стабильная версия WordPress
  • "phpVersion": "8.3" — версия PHP зафиксирована явно
  • "." и WooCommerce — плагины монтируются в окружение
  • WP_DEBUG_LOG — ошибки в лог, а не в браузер

Запуск:

Окно терминала
npx wp-env start
{
"require-dev": {
"pestphp/pest": "^4.0",
"roots/wordpress": "^6.7",
"wp-cli/wp-cli": "*",
"brain/monkey": "^2.4",
"symfony/var-dumper": "^7.4"
},
"extra": {
"wordpress-install-dir": "vendor/wordpress"
}
}
ПакетЗачем
pestphp/pestФреймворк для тестов
roots/wordpressWordPress core как Composer-зависимость
wp-cli/wp-cliCLI-инфраструктура
brain/monkeyМоки WordPress-хуков и функций
symfony/var-dumperУдобная отладка: dd()

Установка внутри контейнера:

Окно терминала
npx wp-env run cli --env-cwd=wp-content/plugins/plugin-name composer install

Pest работает поверх PHPUnit, но с более читаемым синтаксисом:

// Простой тест
it('loads wordpress core functions', function (): void {
expect(function_exists('get_post'))->toBeTrue();
$posts = get_posts();
expect($posts)->toBeArray();
});
// С датасетами
it('validates product types', function (string $type): void {
$product = wc_get_product(wc_create_product(['type' => $type]));
expect($product->get_type())->toBe($type);
})->with(['simple', 'variable', 'grouped']);

Ключевые особенности:

  • it('description', fn () => ...) — читается как предложение
  • expect($value)->toBeX() — текучий API утверждений
  • beforeEach() / afterEach() — подготовка и очистка
  • Датасеты — проверка нескольких вариантов входных данных
<?xml version="1.0"?>
<phpunit
bootstrap="tests/bootstrap.php"
colors="true"
stopOnFailure="false">
<testsuites>
<testsuite name="Plugin Tests">
<directory suffix=".php">./tests/includes/</directory>
</testsuite>
</testsuites>
<php>
<env name="APP_ENV" value="testing"/>
</php>
</phpunit>
<?php
declare(strict_types=1);
$wpLoadPath = '/var/www/html/wp-load.php';
if (! file_exists($wpLoadPath)) {
throw new RuntimeException("WordPress not found at: {$wpLoadPath}");
}
require_once $wpLoadPath;

После этого в тестах доступны:

  • Функции WordPress: get_posts(), get_option(), update_option()
  • Глобальный $wpdb
  • WooCommerce: wc_get_product(), wc_get_order()
  • Функции и классы самого плагина

Регистрация команды wp test:plugin:

<?php
if (defined('WP_CLI') && WP_CLI && class_exists('WP_CLI')) {
WP_CLI::add_command('test:plugin', RunPluginTestsCommand::class, [
'shortdesc' => 'Run plugin tests using Pest.',
]);
}
class RunPluginTestsCommand
{
public function __invoke($args, $assoc_args)
{
$plugin_path = dirname(__DIR__ . '/..');
$pest_binary = $plugin_path . '/vendor/bin/pest';
if (! file_exists($pest_binary)) {
WP_CLI::error("Pest binary not found at {$pest_binary}");
}
$php_binary = defined('PHP_BINARY') ? PHP_BINARY : 'php';
$command_parts = array_merge(
[escapeshellarg($php_binary), escapeshellarg($pest_binary), '--colors=always'],
array_map('escapeshellarg', $args)
);
$command = sprintf(
'cd %s && %s',
escapeshellarg($plugin_path),
implode(' ', $command_parts)
);
passthru($command, $exit_code);
WP_CLI::halt($exit_code);
}
}

Примеры запуска:

Окно терминала
# Полный прогон
npx wp-env run cli wp test:plugin
# Фильтр по имени теста
npx wp-env run cli wp test:plugin --filter="loads wordpress"
# Конкретный файл
npx wp-env run cli wp test:plugin tests/includes/ProductsTests.php
start:
npx wp-env start
start-update:
npx wp-env stop
npx wp-env start --update
stop:
npx wp-env stop
destroy:
npx wp-env destroy
cli:
npx wp-env run cli sh
test:
npx wp-env run cli wp test:plugin

Использование: make start, make stop, make test.

Если плагин синхронизирует данные с внешним сервисом (МойСклад, 1С, CRM) — сохраните реальные JSON-ответы в фикстуры:

tests/data/fixtures-v1/
├── products/
├── categories/
└── variants/

Проверка фикстур:

it('has valid fixtures v1', function (): void {
$fixture = __DIR__ . '/../data/fixtures-v1/products/first-100.json';
expect(file_exists($fixture))->toBeTrue();
$payload = json_decode((string) file_get_contents($fixture), true);
expect($payload)->toBeArray();
expect($payload['rows'] ?? [])->not->toBeEmpty();
});

Преимущества фикстур:

  • Тесты не зависят от сети и доступности API
  • Входные данные стабильны
  • Можно тестировать edge cases (пустой ответ, ошибка API)

9. Транзакции: чистая база после каждого теста

Заголовок раздела «9. Транзакции: чистая база после каждого теста»
beforeEach(function (): void {
global $wpdb;
$wpdb->query('START TRANSACTION');
});
afterEach(function (): void {
global $wpdb;
$wpdb->query('ROLLBACK');
});

После каждого теста база возвращается в исходное состояние. Особенно важно, когда тест создаёт реальные записи в WordPress и WooCommerce.

it('loads plugin and WooCommerce', function (): void {
expect(function_exists('MyPlugin\\init'))->toBeTrue();
expect(function_exists('wc_get_product'))->toBeTrue();
expect(class_exists('WooCommerce'))->toBeTrue();
});

Интеграционный тест синхронизации продукта

Заголовок раздела «Интеграционный тест синхронизации продукта»
it('syncs products with attributes', function (): void {
\MyPlugin\Settings::setValue('attributes_sync_enabled', 1);
$rows = getProductsFixtureRows();
$row = $rows[0];
$productId = \MyPlugin\Products\product_update($row, []);
expect($productId)->toBeInt()->toBeGreaterThan(0);
$product = wc_get_product($productId);
expect($product)->not->toBeFalse();
expect($product->get_meta('external_id'))->toBe((string) $row['id']);
$attrs = $product->get_attributes();
expect($attrs)->toBeArray()->not->toBeEmpty();
});
it('uses ruble currency', function (): void {
expect((string) get_option('woocommerce_currency'))->toBe('RUB');
expect((string) get_option('woocommerce_default_country'))->toBe('RU');
expect((string) get_option('woocommerce_price_num_decimals'))->toBe('2');
});
tests/
├── bootstrap.php # Подключение WordPress
├── add-wp-cli.php # Регистрация WP-CLI команд
├── data/
│ └── fixtures-v1/ # JSON-фикстуры
│ ├── products/
│ ├── categories/
│ └── variants/
└── includes/ # Активные тесты
├── BaseTests.php
├── ProductsTests.php
└── SyncTests.php
ПроблемаРешение
Регрессии при изменении логикиТесты ловят ошибки до релиза
Зависимость от внешнего APIФикстуры дают стабильные входные данные
Ручная настройка WordPresswp-env поднимает окружение одной командой
Сложный запуск тестовWP-CLI + Makefile — короткий интерфейс
Загрязнение БДТранзакции + rollback
Тесты не видят WordPresswp-load.php загружает всё

Стек Pest + WP-CLI + wp-env даёт понятный workflow для тестирования WordPress-плагинов. Для простого плагина может показаться избыточным, но если внутри есть синхронизация данных, WooCommerce, внешние API и бизнес-логика — окупается быстро.