Wenn Sie jemals erlebt haben, dass ein Plugin lokal „funktionierte“ und dann nach einem kleineren Update stillschweigend nicht mehr funktionierte, WordPress In Version 6.9.4 liegt das Problem selten an einer einzelnen Zeile. Es resultiert vielmehr aus fehlenden reproduzierbaren Tests und insbesondere aus einer mangelnden Trennung zwischen reiner Logik und WordPress-Integration.

Was wir bauen werden

Sie werden eine moderne Testumgebung (April 2026) für ein WordPress-Plugin einrichten, das mit PHP 8.1+ und WordPress 6.9.4+ kompatibel ist und aus zwei Ebenen besteht:

  • Unit-Tests (schnell): Testen Sie Ihre PHP-Logik, ohne WordPress zu laden.
  • Integrationstests (realistisch): WordPress laden und Hooks, Rollen/Berechtigungen, Nonces und Datenbankinteraktionen validieren über WP_UnitTestCase.

Das Endergebnis: ein Beispiel-Plugin „BPCAB Demo“ mit einer testbaren Architektur (Dienste + einfache Dependency Injection), einer PHPUnit-Suite und einer GitHub Actions-Pipeline, die Tests auf einer PHP/WP-Matrix ausführt.

Am Ende werden Sie es wissen:

  • Entwickeln Sie ein Plugin, das die Logik auch ohne WordPress testbar macht.
  • Installieren und konfigurieren Sie die WordPress-Testsuite ordnungsgemäß über Composer.
  • Schreiben Sie zuverlässige Tests (Unit- und Integrationstests), die keine Fehler verursachen.
  • Automatisieren Sie die CI-Ausführung mit einer Matrix und Caches.

Kurze Zusammenfassung

  • Wir trennen uns domaine (reines PHP) und Infrastruktur (WP, DB, REST, Admin-Hooks).
  • Der Komponist verwaltet Autoload und Entwicklungsabhängigkeiten (phpunit/phpunit).
  • Zwei PHPUnit-Suites: Einheit (ohne WP) und Integration (mit WP).
  • Der Integrations-Bootstrap lädt WordPress über den WP-Testsuite und konfiguriert die Testdatenbank.
  • Auf CI führen wir eine Matrix aus PHP 8.1/8.2/8.3 + WP 6.9.4: (und schließlich allabendlich (wenn Sie gerne gefährlich leben).

Wann sollte diese Lösung verwendet werden?

  • Sie pflegen ein geschäftskritisches Plugin (E-Commerce, Mitgliederverwaltung, SEO, Formulare, Synchronisierung).
  • Bei WordPress/PHP-Updates treten Regressionen auf.
  • Ihr entwickelt euch als Team weiter und wollt PRs, die weniger Fehler verursachen.
  • Sie verwenden komplexe Hooks (Prioritäten, kumulative Filter, Shortcodes, REST).
  • Sie streben die Einzahlung an. WordPress.org Und Sie wollen ein Mindestmaß an Strenge.

Wann diese Lösung NICHT verwendet werden sollte

  • Ihr „Plugin“ ist ein einfacher 15-zeiliger Code-Schnipsel, der in ein Snippets-Plugin eingefügt wurde (und selbst dann: Ein Unit-Test kann zwar noch nützlich sein, aber der Aufwand ist unverhältnismäßig).
  • Sie arbeiten an einem einmaligen Projekt, das keine Wartung erfordert (selten bei WordPress, aber es kommt vor).
  • Sie weigern sich, Composer einzuführen: Ohne Autoload und ohne Entwicklungsabhängigkeiten werden Sie Ihre Zeit mit Herumprobieren verschwenden.
  • Du hast keine isolierte Umgebung (Docker/Testdatenbank). Ich habe schon erlebt, dass Tests in der Produktionsumgebung ohne Backups schiefgehen.

Bevor Sie beginnen (Voraussetzungen)

Versionen und Umgebung

  • WordPress : 6.9.4 (Ziel) oder höher.
  • PHP Mindestens 8.1 (empfohlen), 8.2/8.3 auch in Ordnung.
  • Komponieren : 2.x.
  • MySQL / MariaDB : eine speziell für Tests entwickelte Datenbank (z. B.: wp_tests).

Datensicherung und Isolation

  • Zeigen Sie nicht. nie die Testkonfiguration für Ihre Produktionsdatenbank.
  • Erstellen Sie nach Möglichkeit einen eingeschränkten Datenbankbenutzer zu Testzwecken.
  • Arbeiten Sie in einem Git-Repository: Sie werden Konfigurationsdateien und Skripte bearbeiten.

Nützliche offizielle Ressourcen


Schritt 1: Erstellen Sie ein testbares Plugin (Gerüst + Autoload)

Der eigentliche Vorteil liegt darin: Wenn Ihre Logik in anonymen Rückruffunktionen feststeckt, testen Sie WordPress anstatt Ihren Code. Ich trenne daher im Allgemeinen:

  • src/Domain : reine Logik (Unit-Test).
  • src/Infrastruktur : WordPress (Hooks, Optionen, REST, Admin).
  • src/Plugin : Bootstrap- und Dienstregistrierung.

1) Erstellen Sie den Plugin-Ordner

Tanz wp-content/plugins/, erstellen:

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

2) Haupt-Plugin-Datei

schaffen 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) Eine Plugin-Klasse + ein Mini-Container

schaffen 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();
	}
}

schaffen 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) Ein Beispiel für reine Logik + einen WP-Hook

schaffen 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} !";
	}
}

schaffen 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']));
	}
}

Erwartetes Ergebnis

Aktivieren Sie das Plugin in ErweiterungsoptionenInstallierte ErweiterungenFügen Sie auf einer Seite Folgendes hinzu:

[bpcab_hello name="Marie Curie"]

Sie müssen unbedingt „Hallo Marie Curie!“ sehen.


Schritt 2: PHPUnit + WordPress-Testsuite (Composer) installieren

Bei WordPress kommt es häufig zu einem Missverständnis: „Ich habe PHPUnit installiert, also kann ich testen.“ Nein. Für Integrationstests benötigen Sie zusätzlich die WP Test Suite (die Dateien). includes/bootstrap.phpFabriken usw.).

1) Composer initialisieren

Im Plugin-Ordner:

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

Ich rate Ihnen, einen Paketnamen anzugeben. vendor/bpcab-demound um zu definieren src/ als Quelle.

2) PHPUnit und PSR-4-Autoloading hinzufügen

Erstellen/Bearbeiten 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"
  }
}

Dann:

composer install
composer dump-autoload

3) Die WordPress-Testsuite abrufen

Es gibt mehrere Methoden. Im Jahr 2026 bleibt die einfachste die beste:

  • Cloner wordpress-develop irgendwo auf Ihrem Rechner,
  • seine Datei verwenden tests/phpunit als Folgetest.

Beispiel (wird angepasst):

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

Sie erhalten dann:

  • ~/wp-tests/wordpress-develop/tests/phpunit
  • ~/wp-tests/wordpress-develop/src (der WP-Kern „entwickelbar“)

Offizielle Quelle zum PHPUnit-Kernansatz: Kernhandbuch: Automatisierte Tests / PHPUnit.


Schritt 3: Schreiben Sie den Test-Bootstrap und isolieren Sie die Umgebung

Wir werden créer zwei PHPUnit-Konfigurationen:

  • Einheit Kein WordPress, schnell, keine Datenbank.
  • Integration WordPress-Lade- und Testdatenbank.

1) PHPUnit-Konfiguration „unit“

schaffen phpunit.unit.xml im Stammverzeichnis des Plugins:

<?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>

schaffen 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 „integration“ konfigurieren

schaffen 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>

schaffen 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';

Erwartetes Ergebnis

In diesem Stadium liefert das Ausführen der Tests noch kein Ergebnis (keine Testdateien), aber:

composer test:unit

PHPUnit muss mindestens gestartet werden.


Schritt 4: Schreiben Sie echte Unit-Tests (ohne WordPress zu laden).

Wir testen BpcabDemoDomainGreetingDieser Test ist schnell, deterministisch und unabhängig vom WP-Kern.

1) Ordner und Test erstellen

schaffen 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) Führen Sie die Unit-Tests aus

composer test:unit

Erwartetes Ergebnis

Sie sollten eine grüne Sequenz erhalten. Falls eine Fehlermeldung wie „Klasse nicht gefunden“ erscheint, liegt das fast immer an Folgendem:

  • fehlerhaftes PSR-4-Autoloading in composer.json,
  • Du hast vergessen composer dump-autoload,
  • Sie haben die Datei im falschen Ordner abgelegt.

Schritt 5: WordPress-Integrationstests (WP_UnitTestCase)

Nun überprüfen wir, ob der Shortcode korrekt registriert ist und die Darstellung korrekt ist. Dazu laden wir WordPress, was zwar langsamer ist, aber so die eigentlichen Fehler aufdeckt.

1) Erstellen Sie einen Integrationstest

schaffen 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) Führen Sie die Integrationstests durch

composer test:integration

Erwartetes Ergebnis

Wenn die Testdatenbank zugänglich ist und dass WP_DEVELOP_DIR ist richtig, Sie erhalten eine grüne Sequenz.

Wenn die Fehlermeldung „Fehler beim Herstellen einer Datenbankverbindung“ angezeigt wird, beheben Sie das Problem nicht willkürlich. Überprüfen Sie zunächst Folgendes:

  • Basis wp_tests existiert,
  • Der DB-Benutzer hat die Rechte.
  • Sie haben nicht angegeben localhost während Ihr MySQL-Server auf Folgendes lauscht 127.0.0.1 (oder umgekehrt, je nach Konfiguration).

Schritt 6: Hooks, Filter, Funktionen und Nonces testen

Die Tests, die die meisten Fehler in der Produktion aufdecken: diejenigen, die überprüfen, ob die Hooks an der richtigen Stelle und mit der richtigen Priorität eingebunden wurden und ob die Sicherheitsvorkehrungen (Nonce/Capabilities) eingehalten werden.

1) Fügen Sie eine sichere Administratoraktion hinzu (Beispiel)

Wir fügen einen sehr einfachen Admin-Aktionsendpunkt hinzu: Er aktualisiert eine Option, jedoch nur für Administratoren und mit einer Nonce.

schaffen 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;
	}
}

Stecken Sie es ein 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) Testkapazität + Nonce

schaffen 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));
	}
}

Dieser Test verdeutlicht einen Punkt, dem ich häufig begegne: Die Admin-Handler beenden schließlich die Ausführung.Im Test kann dies in Folgendes übersetzt werden: wp_die oder eine Ausnahme, abhängig von der Sequenz. Das Muster: Sie prüfen den Zustand (Option, Beitragsmetadaten usw.) anstatt des genauen Ausgabestreams.

Nützliche offizielle Quellen:


Schritt 7: Fabriken, Anlagen, Datenbank und Bereinigung

Fehlgeschlagene Integrationstests entstehen oft durch einen unzureichend bereinigten Datenbankzustand oder durch einen Test, der von einem anderen abhängt. Die WP Test Suite bietet Testumgebungen, aber Disziplin ist unerlässlich:

  • Erstellen Sie Ihre Beiträge/Benutzer/Begriffe im Test.
  • manuelle Optionen/Transienten bereinigen in tearDown(),
  • Verwenden Sie keine fest codierte ID.

Beispiel: Einen Beitrag erstellen und einen Filter testen

Fügen wir einen Filter hinzu, der den Titel eines Beitrags anhand einer Option ändert.

schaffen 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;
	}
}

Stecken Sie es ein Plugin.php wie bei anderen Dienstleistungen.

schaffen 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));
	}
}

Dieser Test schützt Sie vor:

  • ein fehlerhafter Hook (Verwechslung von Aktion und Filter),
  • eine Priorität, die das Ergebnis ändert (falls ein anderes Plugin ihr ebenfalls ein Präfix voranstellt),
  • ein nicht verwalteter Admin-/Frontend-Kontext.

Schritt 8: Automatisierung mit GitHub Actions (WP/PHP-Matrix)

Ohne CI bleibt das Testen „optional“. Mit CI führt eine Regression zu einem Pull Request-Fehler. Genau da beginnt sich der Aufwand auszuzahlen.

1) Einen GitHub Actions-Workflow hinzufügen

schaffen .github/workflows/tests.yml (im Stammverzeichnis des Repos, nicht in wp-content (falls Ihr Plugin ein eigenes Repository ist):

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

Realistische Anmerkungen:

  • Die Tags 6.9.4 in wordpress-develop Existiert, wenn ein Branch/Tag übereinstimmt. Andernfalls verwenden Sie --branch 6.9 oder ein Commit. Passen Sie die Vorgehensweise an die zum Zeitpunkt des Lesens geltende Release-Strategie des Kernsystems an.
  • Wenn Sie WP Nightly hinzufügen, müssen Sie mit vorübergehenden Fehlern rechnen. Ich verwende es oft im Modus „Fehler tolerieren“ für kritische Plugins.

Referenz: offizielle Kernlagerstätte wordpress-develop.


Das vollständige Ergebnis

Wenn Sie alles auf einmal kopieren möchten, finden Sie hier die minimale Zusammenstellung (ohne die bereits angezeigten Dateien). Überprüfen Sie die Pfade bitte noch einmal.

Baum

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

Schnelle Anpassung

  • Fügen Sie eine Suite hinzu Vertrag (Kompatibilitätstests) falls Ihr Plugin öffentliche Filter bereitstellt.
  • hinzufügen phpstan Bei der Entwicklung, wenn Sie die Typen sperren möchten (sehr effektiv bei einer „Domäne/Infrastruktur“-Architektur).
  • Verlagern Sie die Erstellung von Diensten auf einen Dienstanbieter wenn Ihr Plugin wächst.

Anpassen für Divi 5 / Elementor / Avada

Page-Builder hindern Sie nicht am Testen. Der entscheidende Punkt: Testen Sie Ihre Seite. Logik und Ihre WordPress-Integrationennicht die Benutzeroberfläche des Builders (bei der es eher um E2E-Tests geht).

Divi 5

  • Wenn Sie einen Shortcode wie [bpcab_hello]Divi nutzt es einfach über ein „Code“- oder „Text“-Modul.
  • Empfohlener Integrationstest: do_shortcode () (bereits erledigt), und wenn Sie Assets hinzufügen, testen Sie wp_enqueue_scripts (Hook + Abhängigkeiten).

Elementor

  • Für Widget Original ElementorIch trenne die Widget-Klasse (Infrastruktur) und einen „Renderer“-Dienst (Domäne). Der Renderer ist eine Einheit, das Widget wird in der Integration getestet (indem überprüft wird, ob der Registrierungs-Hook aufgerufen wird).
  • Ein häufig auftretender Sonderfall ist, dass der Code zur Widget-Registrierung zu früh ausgeführt wird. Überprüfen Sie die Priorität. elementor/widgets/register (oder ein entsprechender Hook, abhängig von der Elementor-Version).

Avada (Fusion Builder)

  • Dasselbe Prinzip gilt: Avada-Shortcodes/Elemente können Ihre Ausgabe umschließen. Ein hilfreicher Test: Überprüfen Sie, ob Ihr Shortcode eine stabile, maskierte Zeichenkette zurückgibt.
  • Wenn Sie ein „Fusion Element“ bereitstellen, isolieren Sie die Generierung von Optionen (Arrays) und testen Sie es im Unit-Modus.

Endkontrolle

  1. Innerhalb des Plugins: Erweiterungsoptionen → „BPCAB Demo (Testbar)“ aktivieren.
  2. Erstellen Sie eine Seite mit [bpcab_hello name="Test"] Sie sollten „Hallo Test!“ sehen.
  3. In der Befehlszeilenschnittstelle, aus dem Plugin-Ordner:
    • composer test:unit : grüne Suite, sehr schnelle Ausführung.
    • composer test:integration : grüne Suite, langsamer, Datenbank wird verwendet.
  4. Auf GitHub: push → der Workflow „Tests“ wechselt zu den PHP-Versionen der Matrix.

Wenn das Ergebnis nicht den Erwartungen entspricht

Symptom Mögliche Ursache Überprüfung Lösung
PHPUnit: „Klasse nicht gefunden“ Autoload PSR-4 falsch konfiguriert aussehen composer.json und der Dateinamensraum composer dump-autoload, corriger BpcabDemo\src/
Integration: „WordPress-Testordner nicht gefunden“ WP_DEVELOP_DIR unrichtig echo $WP_DEVELOP_DIR / Pfad überprüfen Den richtigen Weg einschlagen wordpress-develop
Integration: „Fehler beim Herstellen einer Datenbankverbindung“ Die Testdatenbank existiert nicht oder die Anmeldedaten sind falsch. Melden Sie sich mit den Zugangsdaten bei MySQL an. Creieren wp_testskorrekter Host/Benutzer/Passwort
Der Shortcode wird im Testmodus nicht gespeichert. Plugin nicht geladen in muplugins_loaded überprüfen tests/bootstrap-integration.php Korrigieren Sie die require .../bpcab-demo.php (Weg)
PHP-Fehler „Aufruf der nicht definierten Funktion add_action()“ in Einheit Ein Unit-Test lädt Code, der von WP abhängig ist. Stacktrace anzeigen Verschieben Sie die Logik nach DomainMocking oder Tests in der Integration

Probleme, die ich häufig bei der Fehlersuche beobachte:

  • Der Code wurde an die falsche Stelle kopiert (z. B.: tests/bootstrap-integration.php einstellen src/).
  • Ein fehlendes Semikolon in einer von Bootstrap geladenen Datei: PHPUnit stoppt, bevor überhaupt ein Test angezeigt wird.
  • Verwechslung von Aktionen und Filtern: Sie verwenden add_action statt add_filter (oder umgekehrt), und der Test „stellt nichts fest“.
  • Du testest in der Produktionsumgebung „nur zum Spaß“: Die Testdatenbank überschreibt Optionen. Das solltest du nicht tun.
  • Ihr Plugin wird zu früh/zu spät geladen: Hooks sind nicht verfügbar oder Abhängigkeiten wurden nicht geladen.

Häufige Fallstricke und Fehler

Fehler Verursachen Lösung
Instabile (fehlerhafte) Integrationstests Gesamtzustand von WP nicht bereinigt (Optionen, Transienten, Hooks hinzugefügt) Reinigen in tearDown()Vermeiden Sie globale Singleton-Prozesse und begrenzen Sie die Nebenwirkungen.
„Header wurden bereits gesendet“ Ein Test löst vor dem Puffern eine Umleitung/einen Abbruch aus. Teste den Endzustand, fange ab wp_redirectVermeiden Sie Assertions auf Headern.
„Headerinformationen können nicht geändert werden“ nur in CI Unterschiede bei PHP-Erweiterungen / Ausgabepufferung Verlassen Sie sich in PHPUnit nicht auf den HTTP-Stream; verwenden Sie dafür End-to-End-Tests.
Ein altes Tutorial empfiehlt veraltete Skripte. Entwicklung der PHPUnit-/WP-Testsuite An der aktuellen Kerndokumentation ausrichten und mit wordpress-develop (April 2026)
Das Snippets-Plugin verursacht Fehler in den Tests. Laufzeitcode, der in die Testumgebung geladen wird Testen Sie Ihr Plugin in einer minimalen Umgebung; deaktivieren Sie unnötige MU-Plugins.
Fehler im Zusammenhang mit veraltetem PHP CI oder lokaler Rechner unter PHP 7.x/8.0 Aktualisieren Sie auf PHP 8.1+ (siehe Unterstützte PHP-Versionen)

Variante / Alternative

Option „ohne WordPress-Testsuite“ (nur Unit)

Wenn Ihr Plugin hauptsächlich aus reiner Logik besteht (Berechnungen, Parsen, Generierung von API-Payloads), können Sie sich auf Folgendes beschränken:

  • Composer + PHPUnit,
  • Unit-Tests auf src/Domain,
  • Die Integrationstests wurden durch eine kleine Reihe manueller „Rauchtests“ ersetzt.

Für ein einfaches internes Plugin ist dies akzeptabel, allerdings geht dadurch die Abdeckung von WP-Hooks, Rollen, Nonces und Verhaltensweisen verloren.

„Fortgeschrittenere“ Option: Integrationstests mit einer temporären Umgebung

Bei komplexen Plugins ziehe ich es oft vor, die Integration in einer temporären Umgebung auszuführen:

  • Docker Compose (MySQL + WordPress + CLI),
  • WordPress-Installation in der Elfenbeinküste,
  • PHPUnit-Tests + gegebenenfalls Playwright/Cypress für End-to-End-Tests.

Es ist zwar schwerer, reduziert aber Überraschungen bei Stapeln kurz vor der Produktion.


Sicherheits-, Leistungs- und Wartungstipps

  • Sicherheit Testen Sie stets auch „abgelehnte“ Pfade (ungültige Berechtigungen, Nonces). Sicherheitslücken entstehen oft dadurch, dass es funktioniert, wenn man Administratorrechte hat.
  • Isolationswerte Die Testdatenbank darf niemals mit anderen Systemen geteilt werden. Nicht einmal lokal.
  • Leistung 80 % der Tests sollten im Unit-Testing enthalten sein. Integrationstests sollten die Assembly validieren und nicht jeden einzelnen Fall wiederholen.
  • Wartung Wenn WordPress 6.9.x weiterentwickelt wird, möchten Sie, dass die CI Ihnen mitteilt, wenn etwas nicht mehr funktioniert, bevor Ihre Benutzer davon betroffen sind.
  • Kompat Wenn Sie mehrere WP-Versionen unterstützen, erstellen Sie eine CI-Matrix (z. B. 6.7, 6.8, 6.9.4) und akzeptieren Sie einige bedingte Anpassungen.

Weitere Infos

  • Füge Tests hinzu auf REST API (Routen, Callback-Berechtigungen, Schemas).
  • Testen Sie Ihre DB-Migrationen (Tabellenerstellung, dbDelta()) in der Integration.
  • Richten Sie ein Dienstanbieter (Reinigerbehälter) und die Verkabelung testen.
  • Füge einen Schritt hinzu statische Analyse (PHPStan/Psalm) + Kodierungsstandards (PHPCS WordPress) auf CI.
  • Wenn Sie JS (Blöcke) haben, fügen Sie Jest/Vitest hinzu über @wordpress/scripts und die Rohrleitungen trennen.

Ressourcen


FAQ

Warum „Unit“- und „Integrations“-Tests trennen, anstatt alles mit geladenem WordPress zu testen?

Da das Laden von WordPress Tests verlangsamt, unzuverlässiger macht und die Fehlersuche erschwert, nutze ich WordPress weiterhin zur Validierung der Assemblierung (Hooks, Rollen, Datenbank) und teste die Geschäftslogik in reinem PHP.

Ist PHPUnit 11 erforderlich?

Nein, aber ab PHP 8.1 ist es eine konsistente und bewährte Wahl. Falls Ihre Systemarchitektur Einschränkungen aufweist, passen Sie die Version entsprechend an. composer.jsonSiehe den Abschnitt zur Kompatibilität in der PHPUnit-Dokumentation.

Warum sollte man wordpress-develop klonen anstatt eines „normalen“ WordPress?

Weil die WP-Testsuite in wordpress-develop/tests/phpunitMan kann natürlich auch auf andere Weise damit herumexperimentieren, aber dabei erfindet man Skripte neu, die bereits vom Kernsystem gepflegt werden.

Meine Integrationstests sind sehr langsam. Was soll ich tun?

Reduzieren Sie deren Anzahl, konzentrieren Sie sie auf Integrationspunkte und verschieben Sie den Rest in Unit-Tests. Verwenden Sie auf CI Composer-Caches und vermeiden Sie es, zu viel Verlauf zu klonen (daher --depth=1).

Wie teste ich einen Hook, der von einer Reihenfolge/Priorität abhängt?

Testen Sie den Endeffekt (Ausgabe/Zustand) und fügen Sie einen Test hinzu, der das Vorhandensein des Callbacks überprüft. has_action() / has_filter() Falls relevant. Hinweis: Diese Funktionen überprüfen die Registrierung, nicht die Ausführung.

Wie testet man Code, der Folgendes tut? exit ?

Im Allgemeinen prüft man den Status (Aktualisierungsoption, Beitrag erstellt) und fängt ab, was man kann (Filter). wp_redirectKämpfe nicht darum, einen "Ausweg" zu erzwingen.

Ich sehe in einem Unit-Test die Fehlermeldung „Aufruf der undefinierten Funktion esc_html()“. Ist das normal?

Ja esc_html() ist eine WordPress-Funktion. Wenn Sie einen Unit-Test wünschen, isolieren Sie die Logik in einer reinen PHP-Klasse und testen Sie das Escaping in der Integration oder injizieren Sie eine Escaping-Strategie (fortgeschrittener).

Kann ich Elementor/Divi/Avada mit PHPUnit testen?

Sie können Ihren Integrationscode testen (z. B. „Mein Widget registriert sich am richtigen Hook“), aber nicht die gesamte Benutzeroberfläche. Verwenden Sie für die Benutzeroberfläche End-to-End-Tests (Playwright/Cypress) auf einer Testumgebung.

Soll ich mich festlegen? vendor/ in meinem Plugin?

Bei Plugins, die außerhalb von Composer vertrieben werden, beinhalten viele Plugins Folgendes: vendor/Für ein internes Plugin, das über Composer verwaltet wird, nein. In allen Fällen installieren Sie die Abhängigkeiten auf dem CI-Server.

Wie lässt sich die Kompatibilität mit mehreren Versionen in WordPress verwalten?

Fügen Sie eine CI-Matrix über mehrere WP-Versionen hinweg hinzu und halten Sie Ihre Unit-Tests unabhängig. Für Kernänderungen verfolgen Sie die Tickets auf [Link einfügen]. Lampenfieber und die PRs auf GitHub.

Was tun, wenn ein alter Tutorial-Ausschnitt unter WordPress 6.9.4 nicht mehr funktioniert?

Erzwingen Sie nichts. Orientieren Sie sich an der aktuellen WordPress-Testsuite (wordpress-develop) und passen Sie Ihr Bootstrap entsprechend an. Veraltete Testinstallationsskripte, die man auf veralteten Blogs findet, funktionieren oft nicht mehr, da PHPUnit (geänderte API) und die Pfade der Testsuite nicht mehr funktionieren.