Ако някога сте виждали плъгин да „работи“ локално и след това тихо да се повреди след малка актуализация на WordPress Във версия 6.9.4 проблемът рядко произтича от един ред. Той идва от липсата на възпроизводими тестове и особено от липсата на разделение между чистата логика и интеграцията с WordPress.

Какво ще изградим

Ще настройвате модерна тестова среда (април 2026 г.) за WordPress плъгин, съвместим с PHP 8.1+ и WordPress 6.9.4+, с две нива:

  • Единични тестове (бързо): тествайте PHP логиката си без да зареждате WordPress.
  • Интеграционни тестове (реалистично): зареждане на WordPress и валидиране на куки, роли/възможности, еднократни стойности и взаимодействия с базата данни чрез WP_UnitTestCase.

Крайният резултат: примерен плъгин „BPCAB Demo“ с тестваема архитектура (услуги + прост DI), пакет PHPUnit и GitHub Actions pipeline, който изпълнява тестове върху PHP/WP матрица.

В крайна сметка ще знаете:

  • Структуриране на плъгин, за да се направи логиката тестваема без WordPress.
  • Инсталирайте и конфигурирайте правилно тестовия пакет на WordPress чрез Composer.
  • Пишете надеждни тестове (модулни + интеграционни), които не се провалят.
  • Автоматизирайте изпълнението на CI с матрица и кешове.

Бързо обобщение

  • Ние се разделяме домейн (чист PHP) и инфраструктура (WP, DB, REST, администраторски куки).
  • Композиторът управлява автоматично зареждане и зависимости от развитието (phpunit/phpunit).
  • Два PHPUnit пакета: единица (без WP) и интеграция (с УП).
  • Интеграционният bootstrap зарежда WordPress чрез WP тестов пакет и конфигурира тестовата база данни.
  • На CI изпълняваме матрица PHP 8.1/8.2/8.3 + WP 6.9.4 (и евентуално всяка вечер (ако обичате да живеете опасно).

Кога да използвате това решение

  • Поддържате „критично важен за бизнеса“ плъгин (електронна търговия, членство, SEO, формуляри, синхронизация).
  • Имате регресии по време на актуализации на WordPress/PHP.
  • Развивате се като екип и искате PR-и, които се провалят по-рядко.
  • Използвате сложни куки (приоритети, кумулативни филтри, кратки кодове, REST).
  • Целите депозита WordPress.org И искате минимално ниво на строгост.

Кога НЕ трябва да използвате това решение

  • Вашият „плъгин“ е прост 15-редов фрагмент, поставен в плъгин за snippets (и дори тогава: единичен тест може все още да е полезен, но инвестицията е непропорционална).
  • Вие сте на еднократен проект без поддръжка (рядко срещано в WordPress, но съществува).
  • Отказваш да въведеш Composer: без автоматично зареждане и без dev зависимости, ще губиш време в човъркане.
  • Нямате изолирана среда (Docker/тестова база данни). Тестване в продукция без резервни копия, виждал съм го да се случва и завършва зле.

Преди да започнете (предварителни изисквания)

Версии и среда

  • WordPress : 6.9.4 (целева) или по-нова версия.
  • PHP : 8.1 минимум (препоръчително), 8.2/8.3 ОК.
  • композирам : 2.x.
  • MySQL / MariaDB : база данни, предназначена за тестване (напр.: wp_tests).

Архивиране и изолация

  • Не сочи никога тестовата конфигурация към вашата производствена база данни.
  • Създайте ограничен потребител на базата данни за целите на тестването, ако е възможно.
  • Работете в Git хранилище: ще манипулирате конфигурационни файлове и скриптове.

Полезни официални ресурси


Стъпка 1: Създайте тестваем плъгин (скелет + автоматично зареждане)

Истинската полза идва от това: ако логиката ви е заседнала в анонимни обратни извиквания, ще „тествате WordPress“, вместо да тествате кода си. Обикновено разделям:

  • src/Домейн чиста логика (единичен тест).
  • src/Инфраструктура WordPress (hooks, опции, REST, администратор).
  • src/Плъгин : bootstrap и регистрация на услуга.

1) Създайте папката с плъгина

категория wp-content/plugins/, създайте:

  • bpcab-demo/
  • bpcab-demo/bpcab-demo.php
  • bpcab-demo/src/
  • bpcab-demo/tests/

2) Главен файл на плъгина

създавам wp-content/plugins/bpcab-demo/bpcab-demo.php :

<?php
/**
 * Plugin Name: BPCAB Demo (Testable)
 * Description: Plugin d'exemple pour tests unitaires + intégration WordPress.
 * Version: 0.1.0
 * Requires at least: 6.9
 * Requires PHP: 8.1
 */

declare(strict_types=1);

if (!defined('ABSPATH')) {
	exit;
}

// Autoload Composer (en dev, et aussi en prod si vous packez vendor/).
$autoload = __DIR__ . '/vendor/autoload.php';
if (file_exists($autoload)) {
	require_once $autoload;
}

add_action('plugins_loaded', static function (): void {
	// Bootstrap minimal. En vrai, je préfère une classe Plugin + container.
	$plugin = new BpcabDemoPluginPlugin(__FILE__);
	$plugin->boot();
});

3) Клас Plugin + мини-контейнер

създавам src/Plugin/Plugin.php :

<?php

declare(strict_types=1);

namespace BpcabDemoPlugin;

use BpcabDemoInfrastructureHooksHelloHook;
use BpcabDemoPluginContainerContainer;

final class Plugin
{
	private string $pluginFile;
	private Container $container;

	public function __construct(string $pluginFile)
	{
		$this->pluginFile = $pluginFile;
		$this->container = new Container();
	}

	public function boot(): void
	{
		// Enregistrement des services.
		$this->container->set(HelloHook::class, function (): HelloHook {
			return new HelloHook();
		});

		// Activation des intégrations WP.
		$this->container->get(HelloHook::class)->register();
	}
}

създавам src/Plugin/Container/Container.php :

<?php

declare(strict_types=1);

namespace BpcabDemoPluginContainer;

use RuntimeException;

final class Container
{
	/** @var array<string, callable> */
	private array $factories = [];

	/** @var array<string, object> */
	private array $instances = [];

	/**
	 * @param callable():object $factory
	 */
	public function set(string $id, callable $factory): void
	{
		$this->factories[$id] = $factory;
	}

	public function get(string $id): object
	{
		if (isset($this->instances[$id])) {
			return $this->instances[$id];
		}

		if (!isset($this->factories[$id])) {
			throw new RuntimeException("Service introuvable: {$id}");
		}

		$instance = ($this->factories[$id])();
		$this->instances[$id] = $instance;

		return $instance;
	}
}

4) Пример за чиста логика + WP hook

създавам src/Domain/Greeting.php :

<?php

declare(strict_types=1);

namespace BpcabDemoDomain;

final class Greeting
{
	public function message(string $name): string
	{
		$name = trim($name);
		if ($name === '') {
			return 'Bonjour !';
		}

		// Cas réel : éviter les espaces multiples, et limiter la taille.
		$name = preg_replace('/s+/', ' ', $name) ?? $name;
		$name = mb_substr($name, 0, 60);

		return "Bonjour {$name} !";
	}
}

създавам src/Infrastructure/Hooks/HelloHook.php :

<?php

declare(strict_types=1);

namespace BpcabDemoInfrastructureHooks;

use BpcabDemoDomainGreeting;

final class HelloHook
{
	public function register(): void
	{
		add_shortcode('bpcab_hello', [$this, 'shortcode']);
	}

	/**
	 * @param array<string,mixed> $atts
	 */
	public function shortcode(array $atts = []): string
	{
		$atts = shortcode_atts(
			[
				'name' => '',
			],
			$atts,
			'bpcab_hello'
		);

		$greeting = new Greeting();

		// Sécurité : sortie échappée.
		return esc_html($greeting->message((string) $atts['name']));
	}
}

Очакван резултат

Активирайте плъгина в РазширенияИнсталирани разширенияНа страница добавете:

[bpcab_hello name="Marie Curie"]

Трябва да видите „Здравей, Мари Кюри!“.


Стъпка 2: Инсталирайте PHPUnit + WordPress тестов пакет (Composer)

В WordPress възниква класическото объркване: „Инсталирах PHPUnit, така че мога да тествам.“ Не. За интеграционно тестване ви е необходим и WP Test Suite (файловете includes/bootstrap.phpфабрики и др.).

1) Инициализирайте Composer

В папката с плъгина:

cd wp-content/plugins/bpcab-demo
composer init

Съветвам ви да въведете тип име на пакет vendor/bpcab-demoи да се дефинира src/ като източник.

2) Добавете автоматично зареждане на PHPUnit и PSR-4

Създаване/редактиране composer.json :

{
  "name": "vendor/bpcab-demo",
  "type": "wordpress-plugin",
  "require": {
    "php": ">=8.1"
  },
  "require-dev": {
    "phpunit/phpunit": "^11.0"
  },
  "autoload": {
    "psr-4": {
      "BpcabDemo\": "src/"
    }
  },
  "autoload-dev": {
    "psr-4": {
      "BpcabDemo\Tests\": "tests/"
    }
  },
  "scripts": {
    "test:unit": "phpunit -c phpunit.unit.xml",
    "test:integration": "phpunit -c phpunit.integration.xml"
  }
}

След това:

composer install
composer dump-autoload

3) Изтеглете тестовия пакет на WordPress

Имате няколко метода. През 2026 г. най-простият остава:

  • клонинг разработка на WordPress някъде на вашата машина,
  • използвайте неговия файл tests/phpunit като последващ тест.

Пример (ще бъде адаптиран):

mkdir -p ~/wp-tests
cd ~/wp-tests
git clone --depth=1 https://github.com/WordPress/wordpress-develop.git

След това ще имате:

  • ~/wp-tests/wordpress-develop/tests/phpunit
  • ~/wp-tests/wordpress-develop/src („развиваемо“ ядро ​​на WP)

Официален източник за подхода от страна на ядрото на PHPUnit: Основно ръководство: Автоматизирано тестване / PHPUnit.


Стъпка 3: Напишете тестовия bootstrap код и изолирайте средата

Отиваме КРЕЕ две конфигурации на PHPUnit:

  • единица Няма WordPress, бърз е, няма база данни.
  • интеграция Зареждане на WordPress + тестова база данни.

1) Конфигурация на PHPUnit „единица“

създавам phpunit.unit.xml в основата на плъгина:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit
	bootstrap="tests/bootstrap-unit.php"
	colors="true"
	failOnRisky="true"
	failOnWarning="true"
	cacheDirectory=".phpunit.cache/unit"
>
	<testsuites>
		<testsuite name="unit">
			<directory suffix="Test.php">tests/unit</directory>
		</testsuite>
	</testsuites>

	<php>
		<ini name="error_reporting" value="-1"/>
	</php>
</phpunit>

създавам tests/bootstrap-unit.php :

<?php

declare(strict_types=1);

// Bootstrap minimal pour tests unitaires : autoload uniquement.
$autoload = dirname(__DIR__) . '/vendor/autoload.php';
if (!file_exists($autoload)) {
	fwrite(STDERR, "Autoload introuvable. Lancez 'composer install'.n");
	exit(1);
}

require_once $autoload;

2) Конфигурирайте „интеграция“ на PHPUnit

създавам phpunit.integration.xml :

<?xml version="1.0" encoding="UTF-8"?>
<phpunit
	bootstrap="tests/bootstrap-integration.php"
	colors="true"
	failOnRisky="true"
	failOnWarning="true"
	cacheDirectory=".phpunit.cache/integration"
>
	<testsuites>
		<testsuite name="integration">
			<directory suffix="Test.php">tests/integration</directory>
		</testsuite>
	</testsuites>

	<php>
		<ini name="error_reporting" value="-1"/>

		<!-- Paramètres DB : ne mettez jamais la prod ici -->
		<env name="WP_TESTS_DB_NAME" value="wp_tests"/>
		<env name="WP_TESTS_DB_USER" value="root"/>
		<env name="WP_TESTS_DB_PASS" value=""/>
		<env name="WP_TESTS_DB_HOST" value="127.0.0.1"/>

		<!-- Chemins vers wordpress-develop (à adapter à votre machine) -->
		<env name="WP_DEVELOP_DIR" value="/home/vous/wp-tests/wordpress-develop"/>
	</php>
</phpunit>

създавам tests/bootstrap-integration.php :

<?php

declare(strict_types=1);

/**
 * Bootstrap d'intégration WordPress.
 * Hypothèse : vous avez cloné wordpress-develop et indiqué WP_DEVELOP_DIR.
 */

$autoload = dirname(__DIR__) . '/vendor/autoload.php';
if (!file_exists($autoload)) {
	fwrite(STDERR, "Autoload introuvable. Lancez 'composer install'.n");
	exit(1);
}
require_once $autoload;

$developDir = getenv('WP_DEVELOP_DIR');
if (!$developDir) {
	fwrite(STDERR, "WP_DEVELOP_DIR manquant. Configurez-le dans phpunit.integration.xml.n");
	exit(1);
}

$testsDir = rtrim($developDir, '/\') . '/tests/phpunit';
if (!is_dir($testsDir)) {
	fwrite(STDERR, "Dossier tests WordPress introuvable: {$testsDir}n");
	exit(1);
}

// Variables attendues par la WP test suite.
$_tests_dir = $testsDir;

// Charge les fonctions utilitaires de la suite.
require_once $_tests_dir . '/includes/functions.php';

/**
 * Charger le plugin avant que WordPress ne finisse son bootstrap de tests.
 * C'est le point standard (muplugins_loaded) utilisé par la suite.
 */
tests_add_filter('muplugins_loaded', static function (): void {
	require dirname(__DIR__) . '/bpcab-demo.php';
});

// Démarre WordPress pour les tests.
require_once $_tests_dir . '/includes/bootstrap.php';

Очакван резултат

На този етап, изпълнението на тестовете все още няма да доведе до нищо (няма тестови файлове), но:

composer test:unit

трябва поне да стартирате PHPUnit.


Стъпка 4: Напишете истински модулни тестове (без да зареждате WordPress)

Тестваме BpcabDemoDomainGreetingТози тест е бърз, детерминистичен и не зависи от ядрото на WP.

1) Създайте папката и теста

създавам tests/unit/GreetingTest.php :

<?php

declare(strict_types=1);

namespace BpcabDemoTestsUnit;

use BpcabDemoDomainGreeting;
use PHPUnitFrameworkTestCase;

final class GreetingTest extends TestCase
{
	public function testMessageSansNomRetourneUneFormeCourte(): void
	{
		$greeting = new Greeting();

		$this->assertSame('Bonjour !', $greeting->message(''));
		$this->assertSame('Bonjour !', $greeting->message('   '));
	}

	public function testMessageNormaliseLesEspaces(): void
	{
		$greeting = new Greeting();

		$this->assertSame('Bonjour Ada Lovelace !', $greeting->message('Ada     Lovelace'));
	}

	public function testMessageLimiteLaLongueurPourEviterDesSortiesAbsurdes(): void
	{
		$greeting = new Greeting();

		$name = str_repeat('A', 1000);
		$result = $greeting->message($name);

		$this->assertStringStartsWith('Bonjour ', $result);
		$this->assertStringEndsWith(' !', $result);

		// "Bonjour " (8) + 60 + " !" (2) = 70
		$this->assertSame(70, mb_strlen($result));
	}
}

2) Изпълнете модулните тестове

composer test:unit

Очакван резултат

Трябва да получите зелена последователност. Ако получите грешка като „Класът не е намерен“, това почти винаги е:

  • неправилно автоматично зареждане на PSR-4 composer.json,
  • забрави composer dump-autoload,
  • Поставил си файла в грешната папка.

Стъпка 5: Тестове за интеграция с WordPress (WP_UnitTestCase)

Сега проверяваме дали шорткодът е правилно регистриран и дали рендирането е коректно. В този момент зареждаме WordPress, така че е по-бавен, но улавя „истинските“ регресии.

1) Създайте интеграционен тест

създавам tests/integration/ShortcodeHelloTest.php :

<?php

declare(strict_types=1);

namespace BpcabDemoTestsIntegration;

use WP_UnitTestCase;

final class ShortcodeHelloTest extends WP_UnitTestCase
{
	public function testShortcodeEstEnregistre(): void
	{
		// Le plugin est chargé via bootstrap-integration.php.
		global $shortcode_tags;

		$this->assertIsArray($shortcode_tags);
		$this->assertArrayHasKey('bpcab_hello', $shortcode_tags);
	}

	public function testShortcodeRendLeTexteAttendu(): void
	{
		$html = do_shortcode('[bpcab_hello name="Marie Curie"]');

		// esc_html() est appliqué, donc pas de HTML.
		$this->assertSame('Bonjour Marie Curie !', $html);
	}
}

2) Изпълнете интеграционните тестове

composer test:integration

Очакван резултат

Ако тестовата база данни е достъпна и че WP_DEVELOP_DIR е правилно, получавате зелена последователност.

Ако видите „Грешка при установяване на връзка с базата данни“, не я поправяйте произволно. Първо проверете дали:

  • ла база wp_tests съществува,
  • Потребителят на базата данни има правата,
  • не си сложил/а localhost докато вашият MySQL слуша 127.0.0.1 (или обратното, в зависимост от конфигурацията).

Стъпка 6: Тествайте кукички, филтри, възможности и еднократни единици

Тестовете, които откриват най-много грешки в продакшън: тези, които потвърждават, че сте включили куките си на правилното място, с правилния приоритет и че сигурността (nonce/capabilities) е спазена.

1) Добавете защитено администраторско действие (пример)

Добавяме много проста крайна точка за администраторско действие: тя актуализира опция, но само за администратори и с nonce.

създавам src/Infrastructure/Admin/SettingsAction.php :

<?php

declare(strict_types=1);

namespace BpcabDemoInfrastructureAdmin;

final class SettingsAction
{
	public const NONCE_ACTION = 'bpcab_demo_save';
	public const OPTION_KEY = 'bpcab_demo_enabled';

	public function register(): void
	{
		add_action('admin_post_bpcab_demo_save', [$this, 'handle']);
	}

	public function handle(): void
	{
		if (!current_user_can('manage_options')) {
			wp_die('Accès refusé', 403);
		}

		check_admin_referer(self::NONCE_ACTION);

		$enabled = isset($_POST['enabled']) && $_POST['enabled'] === '1';

		update_option(self::OPTION_KEY, $enabled ? '1' : '0');

		wp_safe_redirect(admin_url('options-general.php?page=bpcab-demo'));
		exit;
	}
}

Включете го src/Plugin/Plugin.php :

<?php
// ... (extrait) ...

use BpcabDemoInfrastructureAdminSettingsAction;

// ... dans boot() ...

$this->container->set(SettingsAction::class, function (): SettingsAction {
	return new SettingsAction();
});

$this->container->get(SettingsAction::class)->register();

2) Тестов капацитет + еднократно число

създавам tests/integration/SettingsActionTest.php :

<?php

declare(strict_types=1);

namespace BpcabDemoTestsIntegration;

use BpcabDemoInfrastructureAdminSettingsAction;
use WP_UnitTestCase;

final class SettingsActionTest extends WP_UnitTestCase
{
	public function setUp(): void
	{
		parent::setUp();
		update_option(SettingsAction::OPTION_KEY, '0');
	}

	public function tearDown(): void
	{
		// Nettoyage : évite les tests interdépendants.
		delete_option(SettingsAction::OPTION_KEY);
		parent::tearDown();
	}

	public function testActionRefuseSansCapacite(): void
	{
		$userId = self::factory()->user->create(['role' => 'subscriber']);
		wp_set_current_user($userId);

		$_POST = [
			'_wpnonce' => wp_create_nonce(SettingsAction::NONCE_ACTION),
			'enabled' => '1',
		];

		// wp_die() jette une exception dans la suite de tests WP.
		$this->expectException(WPDieException::class);

		do_action('admin_post_bpcab_demo_save');
	}

	public function testActionRefuseSansNonceValide(): void
	{
		$userId = self::factory()->user->create(['role' => 'administrator']);
		wp_set_current_user($userId);

		$_POST = [
			'_wpnonce' => 'nonce_invalide',
			'enabled' => '1',
		];

		$this->expectException(WPDieException::class);

		do_action('admin_post_bpcab_demo_save');
	}

	public function testActionMetAJourOptionAvecNonceEtCapacite(): void
	{
		$userId = self::factory()->user->create(['role' => 'administrator']);
		wp_set_current_user($userId);

		$_POST = [
			'_wpnonce' => wp_create_nonce(SettingsAction::NONCE_ACTION),
			'enabled' => '1',
		];

		// On évite la redirection réelle en interceptant wp_redirect.
		add_filter('wp_redirect', static function (string $location): string {
			// On renvoie une URL neutre, mais on laisse WordPress continuer.
			return $location;
		});

		try {
			do_action('admin_post_bpcab_demo_save');
		} catch (WPDieException $e) {
			// Certains environnements de test peuvent convertir exit en die.
			// On tolère, l'essentiel est l'état final.
		}

		$this->assertSame('1', get_option(SettingsAction::OPTION_KEY));
	}
}

Този тест илюстрира нещо, с което често се сблъсквам: Администраторските манипулатори евентуално излизат от системата (exit().При тестване това може да се преведе като wp_die или изключение в зависимост от последователността. Моделът: проверявате състоянието (опция, мета данни за публикацията и т.н.), а не точния изходен поток.

Полезни официални източници:


Стъпка 7: Фабрики, приспособления, база данни и почистване

Интеграционните тестове, които „дават грешки“, често произтичат от лошо почистено състояние на базата данни или от тест, който зависи от друг. WP Test Suite предоставя фабрики, но трябва да останете дисциплинирани:

  • Създайте вашите публикации/потребители/термини в теста.
  • почистване на ръчни опции/преходни процеси в tearDown(),
  • Не използвайте твърдо кодиран идентификатор.

Пример: създаване на публикация и тестване на филтър

Нека добавим филтър, който променя заглавието на публикацията според зададена опция.

създавам src/Infrastructure/Hooks/TitlePrefixHook.php :

<?php

declare(strict_types=1);

namespace BpcabDemoInfrastructureHooks;

final class TitlePrefixHook
{
	public const OPTION_PREFIX = 'bpcab_demo_title_prefix';

	public function register(): void
	{
		add_filter('the_title', [$this, 'filterTitle'], 10, 2);
	}

	public function filterTitle(string $title, int $postId): string
	{
		$prefix = (string) get_option(self::OPTION_PREFIX, '');
		$prefix = trim($prefix);

		if ($prefix === '' || is_admin()) {
			return $title;
		}

		// Évite de préfixer les titres vides.
		if (trim($title) === '') {
			return $title;
		}

		return $prefix . ' ' . $title;
	}
}

Включете го Plugin.php както и при други услуги.

създавам tests/integration/TitlePrefixHookTest.php :

<?php

declare(strict_types=1);

namespace BpcabDemoTestsIntegration;

use BpcabDemoInfrastructureHooksTitlePrefixHook;
use WP_UnitTestCase;

final class TitlePrefixHookTest extends WP_UnitTestCase
{
	public function tearDown(): void
	{
		delete_option(TitlePrefixHook::OPTION_PREFIX);
		parent::tearDown();
	}

	public function testPrefixAppliqueSurTitreFront(): void
	{
		update_option(TitlePrefixHook::OPTION_PREFIX, '[VIP]');

		$postId = self::factory()->post->create([
			'post_title' => 'Mon article',
			'post_status' => 'publish',
		]);

		// Simule un contexte front.
		$this->go_to(get_permalink($postId));
		$this->assertFalse(is_admin());

		$title = get_the_title($postId);
		$this->assertSame('[VIP] Mon article', $title);
	}

	public function testNePrefixPasSiOptionVide(): void
	{
		update_option(TitlePrefixHook::OPTION_PREFIX, '');

		$postId = self::factory()->post->create([
			'post_title' => 'Mon article',
			'post_status' => 'publish',
		]);

		$this->go_to(get_permalink($postId));

		$this->assertSame('Mon article', get_the_title($postId));
	}
}

Този тест ви предпазва от:

  • лоша кука (объркване между действие и филтър),
  • приоритет, който променя резултата (ако друг плъгин също го поставя преди него),
  • неуправляван администраторски/фронтален контекст.

Стъпка 8: Автоматизиране с GitHub Actions (WP/PHP матрица)

Без непрекъсната интеграция (CI), тестването остава „по избор“. С CI, регресията се превръща в PR провал. Именно тук започва да се отплаща.

1) Добавете работен процес за действия в GitHub

създавам .github/workflows/tests.yml (в основата на хранилището, не в wp-content (ако вашият плъгин е специално хранилище):

name: Tests

on:
  push:
  pull_request:

jobs:
  phpunit:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        php: ["8.1", "8.2", "8.3"]
        wp: ["6.9.4"]
    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_ROOT_PASSWORD: root
          MYSQL_DATABASE: wp_tests
        ports:
          - 3306:3306
        options: >-
          --health-cmd="mysqladmin ping -h 127.0.0.1 -proot"
          --health-interval=10s
          --health-timeout=5s
          --health-retries=10

    steps:
      - uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}
          tools: composer:v2
          coverage: none

      - name: Cache Composer
        uses: actions/cache@v4
        with:
          path: |
            vendor
            ~/.composer/cache
          key: composer-${{ runner.os }}-${{ matrix.php }}-${{ hashFiles('composer.lock') }}

      - name: Install dependencies
        run: composer install --no-interaction --prefer-dist

      - name: Checkout wordpress-develop
        run: |
          mkdir -p $HOME/wp-tests
          cd $HOME/wp-tests
          git clone --depth=1 --branch ${{ matrix.wp }} https://github.com/WordPress/wordpress-develop.git

      - name: Run unit tests
        run: composer test:unit

      - name: Run integration tests
        env:
          WP_DEVELOP_DIR: ${{ env.HOME }}/wp-tests/wordpress-develop
          WP_TESTS_DB_NAME: wp_tests
          WP_TESTS_DB_USER: root
          WP_TESTS_DB_PASS: root
          WP_TESTS_DB_HOST: 127.0.0.1
        run: composer test:integration

Реалистични бележки:

  • Етикетът 6.9.4 в wordpress-develop съществува, ако даден клон/таг съвпада. В противен случай използвайте --branch 6.9 или commit. Адаптирайте според основната стратегия за пускане на продукта към момента, в който четете това.
  • Ако добавяте WP nightly, очаквайте временни сривове. Често го използвам в режим „допустими сривове“ за критични плъгини.

Справка: официален депозит за ядро разработка на WordPress.


Пълният резултат

Ако искате да копирате всичко наведнъж, ето минималното сглобяване (без вече показаните файлове). Проверете отново пътищата.

Дървовидна структура

bpcab-demo/
  bpcab-demo.php
  composer.json
  phpunit.unit.xml
  phpunit.integration.xml
  src/
    Domain/
      Greeting.php
    Infrastructure/
      Admin/
        SettingsAction.php
      Hooks/
        HelloHook.php
        TitlePrefixHook.php
    Plugin/
      Plugin.php
      Container/
        Container.php
  tests/
    bootstrap-unit.php
    bootstrap-integration.php
    unit/
      GreetingTest.php
    integration/
      ShortcodeHelloTest.php
      SettingsActionTest.php
      TitlePrefixHookTest.php

Бързо персонализиране

  • Добавяне на апартамент договор (тестове за съвместимост), ако вашият плъгин предоставя публични филтри.
  • Добави phpstan в процес на разработка, ако искате да заключите типовете (много ефективно с архитектура „Домейн/Инфраструктура“).
  • Преместете създаването на услуги към Доставчик на услуги ако вашият плъгин расте.

Адаптиране за Divi 5 / Elementor / Avada

Конструкторите на страници не ви пречат да тествате. Ключовият момент: тествайте логика и вашето Интеграции с WordPress, а не потребителският интерфейс на конструктора (който е по-скоро за e2e тестване).

Диви 5

  • Ако изложите кратък код като [bpcab_hello]Divi го използва лесно чрез модул „Код“ или „Текст“.
  • Препоръчителен тест за интеграция: do_shortcode() (вече е направено) и ако добавите активи, тествайте wp_enqueue_scripts (кука + зависимости).

Elementor

  • За а джаджа обичай ElementorРазделям класа widget (инфраструктура) и услугата "рендер" (домейн). Рендерът е модул, а widget-ът се тества в интеграция (чрез проверка дали е извикан регистрационният hook).
  • Често срещан краен случай, който виждам, е, че кодът за регистрация на уиджет се изпълнява твърде рано. Тествайте приоритета на elementor/widgets/register (или еквивалентен hook в зависимост от версията на Elementor).

Авада (Fusion Builder)

  • Същата логика важи: шорткодовете/елементите на Avada могат да обгръщат изхода ви. Полезен тест: проверете дали вашият шорткод връща стабилен, екраниран низ.
  • Ако предоставяте „Fusion Element“, изолирайте генерирането на опции (масиви) и го тествайте в unit режим.

Последна проверка

  1. В рамките на плъгина: Разширения → активирайте „BPCAB Демо (Тестируемо)“.
  2. Създайте страница с [bpcab_hello name="Test"] : трябва да видите „Hello Test!“.
  3. В интерфейса на командния ред, от папката с плъгина:
    • composer test:unit зелен апартамент, много бързо изпълнение.
    • composer test:integration зелен пакет, по-бавен, използва се база данни.
  4. В GitHub: push → работният процес „Тестове“ превключва към PHP версиите на матрицата.

Ако резултатът не е такъв, какъвто се очаква

симптом Вероятна причина проверка Решение
PHPUnit: „Класът не е намерен“ Автоматичното зареждане на PSR-4 е неправилно конфигурирано Regardez composer.json и пространството от имена на файлове composer dump-autoload, corriger BpcabDemo\src/
Интеграция: „Тестовата папка на WordPress не е намерена“ WP_DEVELOP_DIR неправилен echo $WP_DEVELOP_DIR / проверете пътя Посочете правилния път към wordpress-develop
Интеграция: „Грешка при установяване на връзка с базата данни“ Тестовата база данни не съществува или идентификационните данни са неправилни Влезте в MySQL, използвайки идентификационните данни създавам wp_testsправилен хост/потребител/парола
Краткият код не се запазва в тестов режим. Плъгинът не е зареден muplugins_loaded Проверяващият tests/bootstrap-integration.php Коригирайте require .../bpcab-demo.php (път)
PHP грешка „Извикване на недефинирана функция add_action()“ в модула Единичен тест зарежда код, зависим от WP Преглед на трасирането на стека Преместете логиката в Domainподигравки или тестване в интеграцията

Проблеми, които често виждам при отстраняване на неизправности:

  • Кодът е копиран на грешното място (напр.: tests/bootstrap-integration.php вложа src/).
  • Липсваща точка и запетая във файл, зареден от bootstrap: PHPUnit спира преди дори да покаже тест.
  • Объркване между действие и филтър: използвате add_action вместо add_filter (или обратното) и тестът „не вижда нищо“.
  • Тествате в продукционна среда „само за да видите“: тестовата база данни презаписва опции. Не правете това.
  • Вашият плъгин е зареден твърде рано/твърде късно: куките не са налични или зависимостите не са заредени.

Често срещани капани и грешки

Грешка Причина Решение
Нестабилни (нестабилни) интеграционни тестове Общото състояние на WP не е почистено (добавени са опции, преходни процеси, куки) Почистване tearDown()избягвайте глобални сингълтони, ограничете страничните ефекти
„Заглавките вече са изпратени“ Тестът задейства пренасочване/изход преди буфериране Тестване на крайното състояние, прехващане wp_redirectИзбягвайте твърденията (assert) в заглавките
„Не може да се променя информацията в заглавката“ само в CI Разлики в PHP разширенията / буферирането на изхода Не разчитайте на HTTP потока в PHPUnit; използвайте end-to-end тестове за това.
Стар урок препоръчва остарели скриптове. Еволюция на PHPUnit / WP тестовия пакет Съгласувайте с текущата основна документация и с wordpress-develop (април 2026 г.)
Плъгинът за snippets нарушава тестовете Кодът по време на изпълнение е зареден в тестовата среда Тествайте плъгина си в минимална среда; деактивирайте ненужните mu-плъгини
Грешка, свързана с остарял PHP CI или локална машина на PHP 7.x/8.0 Надстройте до PHP 8.1+ (вижте Поддържани версии на PHP)

Вариант / алтернатива

Опция „без тестов пакет за WordPress“ (само за модул)

Ако вашият плъгин е предимно чиста логика (изчисления, парсинг, генериране на API полезен товар), можете да се ограничите до:

  • Композитор + PHPUnit,
  • единични тестове на src/Domain,
  • Интеграционните тестове са заменени от малка серия ръчни „димни“ тестове.

Това е приемливо за обикновен вътрешен плъгин, но губите покритие на WP hooks, roles, nonces и behaviors.

„По-напреднала“ опция: интеграционно тестване с ефимерна среда

За сложни плъгини често предпочитам да изпълнявам интеграцията в среда за еднократна употреба:

  • Docker Compose (MySQL + WordPress + CLI),
  • Инсталиране на WordPress в Кот д'Ивоар,
  • PHPUnit тестове + евентуално Playwright/Cypress за e2e.

По-тежък е, но намалява изненадите със стекове близо до производството.


Съвети за безопасност, производителност и поддръжка

  • Сигурност Винаги тествайте „отхвърлените“ пътища (невалидни възможности, еднократни стойности). Грешките в сигурността често произтичат от „работи, когато съм администратор“.
  • Изолация Никога не споделяйте тестовата база данни с нищо друго. Дори не локално.
  • Изпълнение 80% от тестовете да се правят в модулно тестване. Интеграционните тестове трябва да валидират асемблирането, а не да преиграват всеки случай.
  • поддръжка Когато WordPress 6.9.x се развива, искате непрекъсваемата интеграция (CI) да ви каже, че „той се поврежда“ преди вашите потребители.
  • Съвместим Ако поддържате няколко WP версии, създайте CI матрица (напр. 6.7, 6.8, 6.9.4) и приемете някои условни корекции.

За да се отиде по-далеч

  • Добавете тестове на REST API (маршрути, разрешения за обратно извикване, схеми).
  • Тествайте миграциите на базата данни (създаване на таблици, dbDelta()) в интеграцията.
  • Настройте a Доставчик на услуги (контейнер за по-чист препарат) и тествайте окабеляването.
  • Добавяне на стъпка статичен анализ (PHPStan/Псалм) + стандарти за кодиране (PHPCS WordPress) на CI.
  • Ако имате JS (блокове), добавете Jest/Vitest чрез @wordpress/скриптове и разделете тръбопроводите.

ресурси


Често задавани въпроси

Защо да разделяме „единица“ и „интеграция“, вместо да тестваме всичко със зареден WordPress?

Защото зареждането на WordPress прави тестовете бавни, ненадеждни и по-трудни за диагностициране. Запазвам WordPress, за да валидирам асемблитото (hooks, roles, database) и тествам бизнес логиката на чист PHP.

Изисква ли се PHPUnit 11?

Не, но на PHP 8.1+ това е последователен и поддържан избор. Ако вашият стек е ограничен, адаптирайте версията съответно. composer.jsonВижте раздела за съвместимост в документацията на PHPUnit.

Защо да клонирам wordpress-develop вместо „нормален“ WordPress?

Защото тестовият пакет на WP се намира в wordpress-develop/tests/phpunitМожете да го променяте по други начини, но ще преоткривате скриптове, които вече се поддържат от ядрото.

Моите интеграционни тестове са много бавни. Какво трябва да направя?

Намалете броя им, фокусирайте ги върху точките на интеграция и преместете останалите в модулни тестове. В CI използвайте кешовете на Composer и избягвайте клонирането на твърде много история (следователно --depth=1).

Как да тествам кука, която зависи от ред/приоритет?

Тествайте крайния ефект (изход/състояние) и добавете тест, който проверява наличието на обратното извикване с has_action() / has_filter() Ако е приложимо. Забележка: тези функции валидират регистрацията, а не изпълнението.

Как да тестваме код, който прави това exit ?

Обикновено тествате състоянието (опция за актуализиране, публикацията е създадена) и прехващате каквото можете (филтрирате) wp_redirectНе се борете, за да „настоявате за изход“.

Виждам „Извикване на неопределена функция esc_html()“ в модулен тест. Нормално ли е това?

Да: esc_html() е функция на WordPress. Ако искате модулен тест, изолирайте логиката в чист PHP клас и тествайте екранирането в интеграцията или инжектирайте стратегия за екраниране (по-напреднало).

Мога ли да тествам Elementor/Divi/Avada с PHPUnit?

Можете да тествате кода си за интеграция (напр. „моят уиджет се регистрира на правилната кука“), но не и целия потребителски интерфейс. За потребителския интерфейс използвайте цялостни тестове (Playwright/Cypress) на тестов сайт.

Трябва ли да се ангажирам? vendor/ в моя плъгин?

За плъгин, разпространяван извън Composer, много плъгини включват vendor/За вътрешен плъгин, управляван чрез Composer, не. Във всички случаи инсталирате зависимостите на CI.

Как да управлявам съвместимостта с множество версии в WordPress?

Добавете CI матрица в множество WP версии и запазете независимите си модулни тестове. За основни промени, следвайте заявките на сценична треска и представителите на властите GitHub.

Какво да направя, ако стар фрагмент от урок вече не работи на WordPress 6.9.4?

Не го насилвайте. Придържайте се към текущия WP тестов пакет (wordpress-develop) и адаптирайте Bootstrap съответно. Старите скриптове за инсталиране на тестове, намиращи се в остарели блогове, често се счупват поради PHPUnit (променен API) и пътищата към пакетите.