Unit Tests sind eine schon seit Jahrzehnten existierende Möglichkeit, um individuell erstellte Software auf ihre Funktionalität hin zu testen. Sie werden fast ebenso lange auch im Bereich von PHP verwendet – und können und sollten daher ebenfalls für die Entwicklung von WordPress Plugins genutzt werden. Wie das aus meiner Sicht am einfachsten machbar ist, beschreibe ich in diesem Artikel.
Wieso überhaupt?
Vielleicht geht es dir gerade genau wie es mir seit Jahren ging: wozu macht man sich überhaupt die Mühe PHP Unit Tests zu schreiben? Schließlich testet man dort ja nur das, was man selbst entwickelt und auch getestet hat. Man testet sein Plugin vielleicht bereits gegen PHPStan sowie die WordPress Coding Standards oder gar den Plugin Checker – die finden doch alles? Oder?
Nein, denn jeder dieser Tests prüft andere Themen:
- PHPStan: sucht nach potentiellen Fehlern im PHP Code ohne diesen tatsächlich auszuführen. Man findet vor allem Schreibfehler und falsche Typisierungen.
- WordPress Coding Standards: führt eine Prüfung hinsichtlich Vorgaben der WordPress Community zur Schreibweise von PHP Code aus. Hier findet man z.B. falsche Formatierungen, fehlende oder falsch geschriebene Kommentierungen sowie Absicherung von Ein- wie Ausgabe von Inhalten. Auch die Verwendung von nonce-Werten im Code wird geprüft, was zusammen mit den anderen Prüfungen die Sicherheit der Anwendung selbst erhöht.
- Plugin Checker: prüft ebenfalls hinsichtlich der WordPress Coding Standards, allerdings nicht nur den PHP-Code sondern auch die Angaben zum Plugin (
readme.txt) und korrekte Versionierung.
Daher lohnt es sich durchaus PHP Unit Tests zusätzlich zu verwenden. Denn hier wird der PHP-Code des Plugins tatsächlich ausgeführt – etwas was kein anderer der o.g. Tests macht. Auf diesem Weg kann man Probleme in Abläufen und auch logische Fehler finden, die man übersehen hat.
Und ja, beim Schreiben von PHP Unit Tests denkt man „aber das macht mein Code doch schon exakt so – wieso teste ich das noch?“ Du richtest diese Tests auch für dein zukünftiges Ich ein, oder für andere Entwickler die an dem gleichen Code arbeiten. Die Tests prüfen den aktuellen Funktionsstand. Änderungen an diesen Abläufen, sollten sich auch in Änderungen in den PHP Unit Tests wiederspiegeln. Denkt man bei Änderungen nicht daran, wird man durch ein Test-Ergebnis direkt darauf hingewiesen, so dass man die Änderung nochmal überdenkt.
Nachhaltigkeit
Ich verwende seit vergangenem Jahr bei all meinen Plugins sowohl PHPStan, WordPress Coding Standards und den Plugin Checker sowie PHP Unit Tests. Meldet auch nur einer dieser Tests einen Fehler, wird kein Release der Plugins erzeugt. Erst nach Behebung der Meldung, ist ein Release möglich.
Das führt zum Einen für mich selbst zu einer größeren Sicherheit wirklich funktionsfähigen Code rauszugeben, zum Anderen können auch die Anwender meiner Plugins sicher sein, auf keinen groben Fehler in diesen zu stoßen. Eine Folge davon ist übrigens auch, dass sich die Supportanfragen zu meinen Plugins auf Funktionsfragen beschränken. Es kommt selten vor, dass sich jemand wegen einem Fehler meldet – und wenn, dann ist es i.d.R. ein Nebeneffekt eines anderen Plugins gewesen, welches sich eben nicht an diese eigentlich selbstverständlichen Standards hält.
Voraussetzungen
Legen wir aber mal los. Um das hier beschriebene bei deinem eigenen Plugin verwenden zu können, benötigst du lediglich composer, Zugriff per SSH auf dein System sowie Datenbankzugangsdaten zum Anlegen von neuen Datenbanken in deinem Datenbanksystem.
Du solltest es ebenso in deinem Docker Image, deinem virtuellen Server oder auch deinem eigenen Server einsetzen können – das ist völlig dir überlassen. Ich führe die hier beschriebenen Scripte in meinem lokalen Virtuellen Server sowie mit GitHub Actions aus.
Durchführung
Vorbereitungen
Damit ich das Vorgehen zur Einrichtung und Verwendung beschreiben kann, erstelle ich zunächst ein kleines Beispiel-Plugin, welches den sensationell einprägsamen Namen „PHP Unit Test Example“ bekommt. Dessen Datei php-unit-test-example.php bekommt erstmal den folgenden Inhalt:
<?php
/**
* Plugin Name: PHP Unit Test Example
* Description: An example for using PHP Unit test for a WordPress plugin.
* Author: Thomas Zwirner
* License: GPL v3
* License URI: https://www.gnu.org/licenses/gpl-3.0.en.html
* Version: 1.0.0
* Requires PHP: 8.0
*
* @package php-unit-test-example
*/
Damit wird auch etwas zum Testen haben, ergänze ich noch 2 kleine Klassen. Eine wird für mathematische Berechnungen genutzt:
<?php
/**
* File for an object to handle math calculations.
*
* @package php-unit-test-example
*/
/**
* Object for math calculations.
*/
class Math {
/**
* Supplement of numbers.
*
* @param int $num1 The first number.
* @param int $num2 The second number.
* @return int
*/
public static function add( int $num1, int $num2 ): int {
return $num1 + $num2;
}
/**
* Substraction of numbers.
*
* @param int $num1 The first number.
* @param int $num2 The second number.
* @return int
*/
public static function subtract( int $num1, int $num2 ): int {
return $num1 - $num2;
}
/**
* Multiplication of numbers.
*
* @param int $num1 The first number.
* @param int $num2 The second number.
* @return int
*/
public static function multiply( int $num1, int $num2 ): int {
return $num1 * $num2;
}
}
Die andere Klasse ermöglicht die Umwandlung und Prüfung eines Textes:
<?php
/**
* File for an object to handle strings.
*
* @package php-unit-test-example
*/
/**
* Object to handle string.
*/
class Strings {
/**
* The given text.
*
* @var string
*/
private string $text;
/**
* Initialize the object with the text.
*
* @param string $text The text.
*/
public function __construct( string $text ) {
$this->text = $text;
}
/**
* Return the text in upper case.
*
* @return string
*/
public function to_upper_case(): string {
return strtoupper( $this->text );
}
/**
* Return the text in lower case.
*
* @return string
*/
public function to_lower_case(): string {
return strtolower( $this->text );
}
/**
* Return the length of the text.
*
* @return int
*/
public function get_length(): int {
return strlen( $this->text );
}
/**
* Return the reverse string.
*
* @return string
*/
public function reverse(): string {
return strrev( $this->text );
}
/**
* Return whether the text contains a string.
*
* @param string $search The searched string.
* @return bool
*/
public function contains( string $search ): bool {
return str_contains($this->text, $search);
}
}
Die beiden Klassen binde ich in die Haupt-Datei des Plugins ein per:
require_once __DIR__ . '/class-math.php';
require_once __DIR__ . '/class-strings.php';
Zwischenstand
Wir haben jetzt unser Plugin anlegt, welches wir als Beispiel für die PHP Unit Tests verwenden wollen. Es macht bei Aktivierung in WordPress gar nichts. Jetzt geht es also direkt an die Einrichtung der Umgebung für die PHP Unit Tests.
Die Bash-Datei
Damit PHP Unit Tests ausgeführt werden können, benötigen wir eine Bash-Datei. Diese ist relativ lang, weshalb ich empfehlen würde sie einfach aus einem meiner Plugins zu kopieren: https://github.com/threadi/external-files-in-media-library/blob/master/bin/install-wp-tests.sh
Die Datei hat die Aufgabe die Umgebung für die PHP Unit Tests einzurichten. Das macht sie in folgenden Schritten:
- Sie lädt per Subversion das Package vom aktuellen WordPress Core von wordpress.org runter und legt diese im System-eigenen temporären Verzeichnis ab, was i.d.R.
/tmp/ist. Dort wird das Package entpackt, so dass es unter/tmp/wordpress-tests-lib/zu finden ist. - Sie erstellt eine eigene Datenbank im lokalen MySQL-Datenbanksystem.
- Sie erstellt eine
wp-config.phpmit den Datenbank-Zugangsdaten, so dass das WordPress sofort lauffähig ist.
Du solltest an der Datei grundsätzlich erstmal nichts anpassen müssen, es sei denn dein System setzt hier andere Bedingungen. Lege die Datei in einem neue Verzeichnis namens bin als install-wp-tests.sh ab und mache sie per SSH ausführbar:
chmod 755 bin/install-wp-tests.sh
Die composer.json
Legen wir nun die für composer wichtige Datei an. Sie hat folgenden Inhalt:
{
"name": "threadi/php-unit-test-example",
"version": "1.0.0",
"scripts": {
"test": "phpunit",
"test-install": "bash bin/install-wp-tests.sh wordpress_test root 'debian' 127.0.0.1 latest"
},
"autoload-dev": {
"psr-4": {
"PhpUnitTestExample\\Tests\\": "tests/"
}
},
"require-dev": {
"yoast/phpunit-polyfills": "^1.0",
"wp-phpunit/wp-phpunit": "^6.3"
},
"require": {
"php": "^8.0"
}
}
Für PHP Unit Tests wichtig, sind hier folgende Zeilen:
"test": "phpunit",
Diese ergänzt das Kommando composer test, mit dem man später die Tests selbst ausführen wird.
"test-install": "bash bin/install-wp-tests.sh wordpress_test root 'debian' 127.0.0.1 latest"
Diese Zeile ergänzt das Kommando composer test-install, welches die Test-Umgebung initialisiert. Beachte, dass du hier ggfs. die dirt vorliegenden Zugangsdaten zur MySQL-Datenbank hinterlegen musst. Ich nutze in dem Beispiel hier die Standard-Zugangsdaten für eine frische Debian-basierte Linux-Installation.
"autoload-dev": {
"psr-4": {
"PhpUnitTestExample\\Tests\\": "tests/"
}
},
Hier wird definiert, dass alle Tests in einem eigenen Namespace namens PhpUnitTestExample\Tests\ laufen.
"require-dev": {
"yoast/phpunit-polyfills": "^1.0",
"wp-phpunit/wp-phpunit": "^6.3"
},
Hier werden die 2 für PHP Unit Tests notwendigen composer Packages für die Entwicklungsumgebung definiert. wp-phpunit stellt dabei die Basis-Tests für PHP Unit Tests bereit während das yoast-Package die WordPress-spezifischen Tests bereitstellt. Das yoast-Package ist das seit einiger Zeit am weitesten fortgeschrittene Paket für diesen Anwendungsfall.
Solltest du eine bestehende composer.json ergänzen wollen, musst du die o.g. Zeilen an den jeweiligen Stellen dort ergänzen.
Führe nun das folgende Kommando aus, um die Umgebung vorzubereiten:
composer install
Die PHP Unit Tests XML Definition
Den folgenden Code legen wir als phpunit.xml.dist an:
<?xml version="1.0"?>
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="tests/bootstrap.php"
colors="true"
convertDeprecationsToExceptions="true"
>
<testsuites>
<testsuite name="testing">
<directory suffix=".php">./tests/Unit/</directory>
</testsuite>
</testsuites>
</phpunit>
Bei Ausführung von composer test wird PHP Unit diese Datei primär einlesen um zu wissen, wo welche Konfigurationen für die Tests hinterlegt sind. I.d.R. muss man hier nichts weiter anpassen.
Das Verzeichnis für die Tests
In der zuletzt angelegten XML-Datei sehen wir einige Pfade, die wir bisher so nicht wahrgenommen haben:
- tests/bootstrap.php
- tests/Unit/
Wir legen daher dieses Verzeichnis an und speichern dort die bootstrap.php mit folgendem Inhalt:
<?php
/**
* PHPUnit bootstrap file.
*
* @package external-files-in-media-library
*/
define( 'TESTS_PLUGIN_DIR', dirname( __DIR__ ) );
define( 'UNIT_TESTS_DATA_PLUGIN_DIR', TESTS_PLUGIN_DIR . '/tests/Data/' ); // Customize.
// Define WP_CORE_DIR if not already defined
if ( ! defined( 'WP_CORE_DIR' ) ) {
$_wp_core_dir = getenv( 'WP_CORE_DIR' );
if ( ! $_wp_core_dir ) {
$_wp_core_dir = rtrim( sys_get_temp_dir(), '/\\' ) . '/wordpress';
}
define( 'WP_CORE_DIR', $_wp_core_dir );
}
$_tests_dir = getenv( 'WP_TESTS_DIR' );
if ( ! $_tests_dir ) {
$_tests_dir = rtrim( sys_get_temp_dir(), '/\\' ) . '/wordpress-tests-lib';
}
// Forward custom PHPUnit Polyfills configuration to PHPUnit bootstrap file.
$_phpunit_polyfills_path = getenv( 'WP_TESTS_PHPUNIT_POLYFILLS_PATH' );
if ( false !== $_phpunit_polyfills_path ) {
define( 'WP_TESTS_PHPUNIT_POLYFILLS_PATH', $_phpunit_polyfills_path );
}
if ( ! file_exists( "{$_tests_dir}/includes/functions.php" ) ) {
echo "Could not find {$_tests_dir}/includes/functions.php, have you run bin/install-wp-tests.sh ?" . PHP_EOL; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
exit( 1 );
}
// Give access to tests_add_filter() function.
require_once "{$_tests_dir}/includes/functions.php";
/**
* Manually load the plugin being tested.
*/
function _manually_load_plugin() {
require dirname( __FILE__, 2 ) . '/php-unit-test-example.php';
}
tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' );
// Start up the WP testing environment.
require "{$_tests_dir}/includes/bootstrap.php";
In dieser Datei musst du diese eine Zeile anpassen:
require dirname( __FILE__, 2 ) . '/php-unit-test-example.php';
Hier musst du die Haupt-Datei deines eigenen Plugins verwenden.
Zwischenstand
Wir haben nun in unserem Beispiel-Plugin, in dem 2 Klassen zum Testen existieren, die Umgebung für die PHP Unit Tests angelegt. Doch ausführbar sind sie weiterhin nicht.
Erste Ausführung
Testen wir das doch mal mit folgendem Kommando:
composer test
Wir bekommen hier folgenden Fehlermeldung:
Could not find /tmp/wordpress-tests-lib/includes/functions.php, have you run bin/install-wp-tests.sh ?
Script phpunit handling the test event returned with error code 1
Die Meldung zeigt klar, dass die Testumgebung fehlt – ist ja auch richtig, denn wir haben diese noch gar nicht installiert. Führen wir also jetzt das Kommando dazu aus:
composer test-install
Du wirst einen Moment warten müssen, bis das Bash-Script den WordPress Code runtergeladen und die Datenbank vorbereitet hat. Du wirst gefragt werden, ob du die Datenbank wirklich ergänze willst, was du mit „y“ bestätigen musst.
Sollte bei der Ausführung der Installation irgendeine Fehlermeldung kommen, gehe dieser zunächst nach. Möglich wäre z.B., dass die Datenbankzugangsdaten, die in der composer.json hinterlegt sind, nicht stimmen.
Wenn die Installation durchgelaufen ist, kannst du jetzt erstmals die Tests ausführen:
composer test
Jetzt wird diese Fehlermeldung kommen:
Test directory "xy/wp-content/plugins/php-unit-test-example/./tests/Unit/" not found
Und die ist absolut richtig, da wir aktuell weder das Testverzeichnis noch Tests angelegt haben.
Der erste Test
Legen wir also einen ersten Test an. Dazu ergänzen wir das Verzeichnis Unit unter tests und dort eine Datei Supplement.php.
<?php
/**
* File for an object to test supplements.
*
* @package php-unit-test-example
*/
namespace PhpUnitTestExample\Unit;
use Math;
use WP_UnitTestCase;
/**
* Object to tests supplement.
*/
class Supplement extends WP_UnitTestCase {
public function test_supplement(): void {
}
}
Führen wir anschließend nochmal das Kommando aus:
composer test
Es kommt zu folgendem Ergebnis:
There was 1 risky test:
1) Supplement::test_supplement
This test did not perform any assertions
/var/www/clients/client1/web1/web/wp-content/plugins/php-unit-test-example/tests/Unit/Supplement.php:12
OK, but incomplete, skipped, or risky tests!
Tests: 1, Assertions: 0, Risky: 1.
Und das ist auch absolut richig so, denn wir haben zwar eine Test-Datei angelegt, diese testet aber gar nichts.
Also ergänzen wir einen Test in der Funktion test_supplement():
$result = Math::add( 1, 2 );
$this->assertEquals( 3, $result );
Wir übergeben hier 1 und 2 als zu addierenden Zahlen und prüfen anschließend, ob das Ergebnis 3 ist. Das Ergebnis bei Ausführung der Tests ist nun:
OK (1 test, 1 assertion)
Annahmen
In PHP Unit Test arbeitet man zur Prüfung von Ergebnissen mit Annahmen, die im Englischen als Assertions bezeichnet werden. Das composer Package wp-phpunit bringt eine große Menge dafür mit, die jeweils als Funktionen aufgerufen werden können. Beispiele:
- assertIsBool()
- assertIsInt()
- assertTrue()
- assertFalse()
- assertEquals()
Eine Liste findet man z.B. hier: https://docs.phpunit.de/en/12.5/assertions.html
Daneben gibt es auch von dem yoast Package ergänze WordPress-spezifische Annahmen, die man in deren Dokumentation finden kann: https://github.com/Yoast/PHPUnit-Polyfills
Wenn wir also per PHP Unit etwas prüfen möchten, gehen wir von einer Annahme aus, was für eine Art Wert eine Variable haben sollte. In dem ersten Test oben, gehen wir daher von der Annahme aus, dass 1+2=3 ist und die Rückgabe von add() folglich den Wert 3 haben sollte.
Genauigkeit
Die Annahmen sind typengenaue Prüfungen. Würde man bei assertEquals() einen String mit einem Integer-Wert vergleichen, würde es zu einem Fehler kommen.
Wenn Projekte sich weiterentwickeln, ändert man manchmal das Rückgabeformat einer Funktion ohne zu bedenken, dass diese möglicherweise noch woanders genutzt werden könnte. Das würde bei Verwendung des Plugins ohne PHP Unit Tests, die das als Fehler melden, zu Fehlermeldungen beim Anwender führen.
Ich habe mir daher angewöhnt jeden Rückgabewert in den Tests auch hinsichtlich ihres Typs zu testen, auch wenn das beim Lesen des Codes wie ein unnötiger Aufwand wirkt. Ich passe den ersten Test daher mal folgendermaßen an:
$result = Math::add( 1, 2 );
$this->assertIsInt( $result );
$this->assertEquals( 3, $result );
Würde die Funktion add() plötzlich einen String zurückgeben, würde es bei assertIsInt() bereits eine Fehlermeldung beim Test geben. Der nachfolgende Test würde dadurch gar nicht erst ausgeführt werden.
Der zweite Test
Mit dem Wissen aus dem ersten Test fällt es nun leicht einen weiteren Test anzulegen. Jetzt möchten wir die Multiplikation testen:
<?php
/**
* File for an object to test multiplications.
*
* @package php-unit-test-example
*/
namespace PhpUnitTestExample\Unit;
use Math;
use WP_UnitTestCase;
/**
* Object to tests multiplications.
*/
class Multiply extends WP_UnitTestCase {
/**
* Test the multiplication for 3*4=12.
*
* @return void
*/
public function test_multiply(): void {
$result = Math::multiply( 3, 4 );
$this->assertIsInt( $result );
$this->assertNotEquals( 10, $result );
}
}
Hier testen wir, ob der Wert 10 nicht mit dem Ergebnis 12 bei der Multiplication von 3 und 4 übereinstimmt. Wir führen die Tests wieder aus:
composer test
Das Ergebnis ist eindeutig richtig, da keine Fehler gemeldet werden. Man sieht aber auch, dass nun 2 Tests und 4 Annahmen ausgeführt werden:
OK (2 tests, 4 assertions)
Zwischenstand
Wir haben unsere ersten PHP Unit Tests angelegt. Diese sind einfach gehalten, verdeutlichen jedoch die Verwendung von Testmöglichkeiten recht anschaulich. Wir wissen nun auch, wie man die Tests per Hand ausführt.
Automatisierung in GitHub
Um die PHP Unit Tests in GitHub per Action auszuführen benötigen wir eine yml-Datei, die die Action definiert:
name: Testing
on:
pull_request:
branches:
- master
jobs:
phpunit:
name: Run tests
runs-on: ubuntu-latest
strategy:
matrix:
php-version: ['8.4']
services:
database:
image: mysql:latest
env:
MYSQL_DATABASE: wordpress_tests
MYSQL_ROOT_PASSWORD: root
ports:
- 3306:3306
steps:
- name: Check out source code
uses: actions/checkout@v6
- name: Run package installs and builds
run: |
composer install
composer update
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
tools: phpunit-polyfills:1.1
extensions: mbstring, xml, zip, intl, pdo, mysql
coverage: none
- name: Install SVN
run: sudo apt-get update && sudo apt-get install -y subversion
- name: Setup tests
run: bash bin/install-wp-tests.sh wordpress_tests root root 127.0.0.1 latest true
- name: Run tests
run: composer test
Diese Action führt quasi das aus, was du bisher manuell an deiner Konsole gemacht hast:
- Vorbereitung der Umgebung
- composer-Pakage initialisieren.
- PHP Unit Test Umgebung initialisieren.
- PHP Unit Tests ausführen.
Sollte es bei einem davon, und besonders beim letzten Punkt, zu einem Fehler kommen, würde die Action das sofort melden.
Was haben wir erreicht?
Ich habe dir hier gezeigt, wie du PHP Unit Tests in deinem WordPress-Plugin einrichten kannst. Du bist damit in der Lage das in deinen eigenen Plugins zu adaptieren. Damit machst du nicht nur dein Plugin sicherer sondern auch für deine Anwender zuverlässiger in der Verwendung.
Das Plugin mit all seinen Dateien und den Tests kannst du hier einsehen: https://github.com/threadi/php-unit-test-example/
Empfehlung: vergiss nicht dein Plugin auch gegen PHPStan, WordPress Coding Standards und den Plugin Checker zu testen.
FAQ
Solange es sich hierbei um nicht-produktiv genutzte öffentliche Testsysteme handelt, kann man diese Angaben hier durchaus hinterlegen. Ohne diese Angaben, könntest du z.B. auch keine GitHub Action für die Ausführung der PHP Unit Tests laufen lassen.
Solltest du dir dessen dennoch unsicher sein, kannst du sie auch hier weglassen und einen anderen Weg finden.
Es gibt hier theoretisch keine Grenzen. Ich selbst habe Projekte mit mehreren tausend PHP Unit Tests, die erfolgreich durchlaufen.
Hierfür eigenen sich dataProvider. Siehe dazu: https://backendtea.com/post/phpunit-data-providers/
Gerne kannst du einfach in das Repository von jedem meiner Plugins schauen, z.B. Externe Dateien in der Mediathek.
PHP Unit Tests prüfen, wie der Name schon sagt, nur PHP-Code. Natürlich kannst du mit einem Test auch prüfen, ob deine Block Scripte eingebunden werden. Mehr aber auch nicht.
Ja, du solltest jedoch bei Verwendung von GitHub hierfür eines Bedenken: ohne weitere Konfiguration gelangen die Test-Dateien und auch die Bash-Datei in das Release von deinem composer Package.
Um das zu verhindern, erstelle eine Datei .gitattribute mit folgendem Inhalt:
# Set the default behavior, in case people don't have core.autocrlf set.
* text=auto
# Do not export these files and directories in the composer package
/.github export-ignore
/bin export-ignore
/tests export-ignore
Dadurch werden diese Verzeichnisse beim Erstellen des Packages ignoriert.