Unit Tests für PHP Code … Ohne das Laden von WordPress – Teil 1

In diesem Post geht es tief in das Thema von Unit Tests in WordPress … ohne WordPress zu laden.

Unit Tests sind eine Art automatisierter Softwaretests, bei der die getesteten Dinge einzelne “Units” einer ganzen Anwendung sind. Wenn wir über PHP Anwendungen reden, ist eine einzelne “Unit” mit großer Wahrscheinlichkeit eine Funktion oder im Falle von objekt-orientiertem Code eine Klasse.

In diesem Artikel werde ich keine “akademischen” Definitionen und technische Beschreibungen darüber machen, was Unit Tests sind und was sie von anderen Testarten unterscheidet. Wenn du das Konzept allerdings vertiefen möchtest, dann ist ein guter Anfang dieser Artikel meines Inpsyde Kollegen Thorsten Frommen.

100% Unit Tests Saft

Wenn ich in einem Satz erklären müsste, was Unit Tests sind, würde ich sagen:

Nimm die kleinste Portion Code, die vom Rest der Anwendung herausgezogen werden kann, führe sie aus, hole dir Feedback über das erhaltene Ergebnis und was das erwartete Ergebnis ist.

In seinem mittlerweile sehr berühmten “TestFrameworkInATweet” von Mathias Verraes bietet er eine Funktion an, die als sehr einfacher, präziser und sauberer Weg gilt, Unit Tests für PHP Code auszuführen

Eine leicht verbesserte Version davon (Das Original ist oben verlinkt) sieht folgendermaßen aus:

<?php
function it($m,$p){ $d=debug_backtrace(0)[0];
    is_callable($p) and $p=$p();
    global $e;$e=$e||!$p;
    $o="e[3".($p?"2m✔":"1m✘")." It $me[0m";
    echo $p?"$on":"$o FAIL in: {$d['file']} #{$d['line']}n";
}

register_shutdown_function(function(){global $e;$e and die(1);});

Jetzt, da wir 280 Zeichen haben, passt es immer noch in einen Tweet (Mit Leerzeichen und allem) und ich kann dir sagen, dass dieser auf Tweetlänge begrenzter Code alles ist, was du brauchst, um Unit Tests laufen zu lassen.

Ein Anwendungsbeispiel über das Laufen lassen eines Unit Tests:
<?php
// the file that contains the 9 lines of code above
require_once __DIR__.'/test-framework-in-a-tweet.php';

it( 'should sum two numbers.', 1 + 1 === 2 );

it( 'should display an X for a failing test.', 1 + 1 === 3 );

it( 'should append to an ArrayIterator.', function () {
    $iterator = new ArrayIterator();
    $iterator->append( 'test' );
    return
        count( $iterator ) === 1
        && $iterator->current() === 'test';
} );

Die (farblich markierte!) Ausgabe wird sein:

✔ It should sum two numbers.
✘ It should display an X for a failing test. FAIL in: /path/to/test-file.php #3
✔ It should append to an ArrayIterator.

Es endet mit einem Exit Code 1, falls Fehler auftreten. Das macht das Framework integrierbar mit anderen Tools zur Qualitätskontrolle.

Logischerweise ist das, was dieses “Mini Framework” tut, folgendes: akzeptiert eine Beschreibung des Tests und ein “Prädikat”. Das ist etwas, dass wir für wahr erklären und dann sagt es uns, ob das, was wir für wahr erklärt haben auch tatsächlich wahr ist.

Das ist alles über Unit Tests.

Eine Bibliothek wie PHPUnit mit ihren 41.000+ Zeilen von Code und ihren 7000+ Klassen ist ein bisschen mehr als nur “Schmuck” und Hilfestellungen für dieses Core Konzept.

Leitung nach Userland

Im vorangegangenen Beispiel haben wir nur einige arithmetische Operationen und eine einzelne Klasse, die von PHP bereitgestellt wird, getestet. Eigentlich haben wir also getestet, dass PHP keine Bugs in diesen Dingen hat.

Keine Abhängigkeiten sind involviert, eigentlich ist kein Userland Code in dem, was wir getestet haben, involviert (außer dem “Framework” selbst).

Allerdings können wir mit unserem Mikro-Framework Userland Code effektiv testen.

Nimm diese Klasse:

<?php
namespace MyCompanyMyPlugin;

final class Email {

    private $email;

    public function __construct( string $email ) {
        if ( ! filter_var( $email, FILTER_VALIDATE_EMAIL ) ) {
            throw new InvalidArgumentException(
                "{$email} is not a valid email." );
            }
            $this->email = $email;
}

    public function __toString(): string {
        return $this->email;
     }

    public function equals( Email $email ): bool {
        return (string) $this === (string) $email;
    }
}

Das ist das Beispiel eines Value Object. Wir können es einfach mit unserem Framework testen:

<?php
require_once '/path/to/test-framework-in-a-tweet.php';
require_once '/path/to/code/src/MyCompany/MyPlugin/Email.php';


it( 'should fail for invalid emails.', function () {
    try {
        new MyCompanyMyPluginEmail( 'foo' );
    } catch ( InvalidArgumentException $e ) {
        return true;
    }
} );


it( 'should equal a string when casted to string.', function () {
    $email = new MyCompanyMyPluginEmail( 'foo@example.com' );
    return (string) $email === 'foo@example.com';
} );


it( 'should equal another instance by value.', function () {
    $email = new MyCompanyMyPluginEmail( 'foo@example.com' );
    return $email->equals( new Email( 'foo@example.com' ) );
} );

und die “grünen” Ergebnisse:

✔ It should fail for invalid emails.
✔ It should equal string value when serialized.
✔ It should equal another instance by value.

Wir haben bewiesen, dass unser Code ohne die Hilfe von irgendeinem “aufgeblähten” Test Framework gut funktioniert.

WordPress in dem Ganzen

Alle Beispiele oben beinhalten nur “pures” PHP: Keine externe Bibliothek wird benötigt, um den Code oder die Tests auszuführen.

Aber was ist mit WordPress? Viele WordPress Entwickler sind daran gewöhnt, Unit Tests für ihre Plugins in der gleichen Weise zu schreiben wie WordPress Core Entwickler WP PHPUnit Test Suite schreiben. Ein WP CLI command existiert, um so geschriebene Plugin Tests zu scaffolden.

Wenn unsere Email Klasse von oben ein Teil eines WordPress Plugins wäre und wir diesen Workflow nutzen würden, würden wir:

  • eine ziemlich “teure” Scaffolding Prozedur
  • eine Datenbank errichten müssen
  • die gesamte WordPress Umgebung laden müssen

all das, um etwas zu testen, von dem wir gesehen haben, dass es mit einem 280-Zeichen Framework effektiv und komplett getestet werden kann.

Also, macht es Sinn, Unit Tests “in the WordPress Way” für Code laufen zu lassen, der keine WordPress Funktionalitäten nutzt, selbst wenn es ein Teil eines Plugins (oder Themes) ist? Die Antwort ist ein klares Nein. Wenn kein WordPress Code involviert ist, nutze ein Test Framework deiner Wahl (PHPUnit, phpspec, Codeception… oder sogar unser “TestFrameworkInATweet“) und teste das Ding einfach.

Aber jetzt lasst uns ein interessanteres Beispiel anschauen:

<?php
namespace MyCompanyMyPlugin;

function register_product_cpt() {
    register_post_type( 'product', [ /* ...bunch of args... */ ] );
}

Wenn wir versuchen diese Funktion zu testen, ohne WordPress zu laden, werden wir ziemlich scheitern, weil es eine WordPress Funktion verwendet, die nicht definiert werden würde, wenn WordPress nicht geladen wird.

Bedeutet das jetzt, dass wir dazu “verdammt” sind, WordPress zu laden, um diese beiden Funktionen zu testen? Werden wir wirklich eine ganze WordPress Installation (Konfiguration und Datenbank inbegriffen) scaffolden müssen, selbst wenn unser Code gar nichts davon braucht? Müssen wir wirklich die ganze WordPress Umgebung mit ihren aberhunderten Zeilen von Code und ihre Nebeneffekte für jeden einzelnen Test laden müssen, wenn das Testziel eine Funktion ist, die nur eine einfache WordPress Funktion verwendet?

Zum Glück nicht. Drei Mal Nein.

Unit oder nicht Unit: Ist das die Frage?

Lasst uns für einen Moment vorstellen, dass wir unser Mikro-Framework nutzen möchten, um die  register_product_cpt Funktion im Kontext von WordPress zu testen. Wie sieht das aus?

Vielleicht etwas ähnlich zu dem hier:

<?php
require_once __DIR__.'/test-framework-in-a-tweet.php';
// This load the whole WP environment
require_once '/path/to/wordpress/wp-load.php';


it( 'should register product post type.', function () {
  
$exists_before = post_type_exists( 'product' );
  
MyCompanyMyPluginregister_product_cpt();
  
$exists_after = post_type_exists( 'product' );
  
return $exists_before === false && $exists_after === true;
} );

Es gibt bessere Entwickler als ich, die dir sagen werden, dass das, was du oben siehst, kein Unit Test ist, weil er eigentlich Code von WordPress ausführt. Also ist es ein Integration Test, weil es ein Testen im Hinblick auf die Frage ist, ob unser benutzerdefinierter Code sich gut in WordPress integriert.

Allerdings gibt es andere Entwickler, die ebenfalls besser als ich sind, die dir sagen werden, dass der Test oben ein Unit Test ist. Weil das System, das wir testen, also das Ding, was wir testen, immer noch eine einzelne Unit der Anwendung ist  (die register_product_cpt Funktion) und was wir tun ist eine “State Verification”, also, wir vergleichen den Status der Anwendung bevor und nachdem die einzelne Unit ausgeführt worden ist.

Wenn du (bisher) keine Ahnung hast, welche der beiden Parteien Recht hat: Das ist nicht weiter schlimm, weil es für das Ziel dieses Artikel kein bisschen relevant ist.

WordPress und Unit Tests: Was testen wir da eigentlich?

Was definitiv klar ist: Tests in dieser Art und Weise auszuführen bedeutet, dass wir eine WordPress Umgebung (die Anwendung konfigurieren, eine Datenbank vorbereiten, Nebeneffekte wie Cron Tasks triggern …) einrichten müssen und wir müssen “warten”, weil WordPress hunderte von Dateien läd, selbst wenn wir nur eine Funktion brauchen …

… Aber am Ende funkioniert es, oder nicht?

Die ware Frage ist: Was testen wir hier?

Unsere register_product_cpt Funktion ist ohne Logik. Sie ist eine Verpackung um eine WordPress Funktion.

Wenn der Test läuft, testen wir effektiv, ob:

  1. unsere Funktion die WordPress Funktion auf die richtige Art und Weise aufruft
  2. die WordPress Funktion tut, was wir erwarten, dass sie tut

Aber, müssen wir wirklich testen, ob die WordPress Funktion ihren Job macht? Ist das nicht die Aufgabe eines WordPress Core Developers? Wenn wir unser Plugin testen, sollten wir dann nicht nur unser Plugin testen müssen, mehr nicht?

Lasst uns für einen Moment die Perspektive wechseln. In unserem gesamten Code gehen wir davon aus, dass PHP funktioniert. Wenn wir eine Zeile wie diese hier schreiben:

return $cpt_exists_before === false && $cpt_exists_after === true;

dann vertrauen wir PHP, dass es einen Wert zurückgibt, wenn wir return eingeben, und wir vertrauen, dass es die Logik richtig berechnet … im Code, den wir jeden Tag schreiben, vertrauen wir PHP Dinge zu tun, die viel komplexer als das hier sind.

Wenn ein Test, den wir geschrieben haben, scheitert, ist es sicherlich möglich, dass er wegen eines PHP Bugs (PHP ist Code und Code ohne Fehler existiert nicht) scheitert. Aber wir gehen nicht davon aus, dass PHP in unseren Unit Tests Fehler hat.

Angesichts der Tatsache, dass wir ein WordPress Plugin schreiben und WordPress wie PHP ein Teil der Infrastruktur unserer Anwendung ist, können wir nicht das gleiche für WordPress machen? Können wir nicht davon ausgehen, dass WordPress funktioniert, so wie wir es auchc für PHP tun?

Wenn die Antwort zu den Fragen oben “Ja” ist und wir davon ausgehen können, dass WordPress ohne die Last des Ladens funktioniert, wäre das nicht großartig?

Was ist, wenn wir den Test so umschreiben:

<?php
function post_type_exists( $post_type ) {
    return array_key_exists( "post_type_{$post_type}", $GLOBALS );
}

function register_post_type( $post_type, $args ) {
    $GLOBALS[ "post_type_{$post_type}" ] = $args;
}


it( 'should register product post type.', function () {
  
    $exists_before = post_type_exists( 'product' );
  
    MyCompanyMyPluginregister_product_cpt();
  
    $exists_after = post_type_exists( 'product' );
  
    return $exists_before === FALSE && $exists_after === TRUE;
} );


it( 'should register product as not hierarchical.', function () {
  
    MyCompanyMyPluginregister_product_cpt();
  
    global $post_type_product;
  
    return
        is_array( $post_type_product )
        && array_key_exists( 'hierarchical', $post_type_product )
        && $post_type_product[ 'hierarchical' ] === FALSE;
} );

Weil wir WordPress dieses Mal nicht laden, können wir unsere eigene vereinfachte Version von post_type_exists und register_post_type schreiben.

In Fakten – mit dem Code oben könnten wir testen, dass register_product_cpt:

  • wirklich den register_post_type aufruft und den erwarteten CPT Namen übergibt
  • das Argument, dass an register_post_type übergeben wurde, enthält das "hierarchical" Argument, das auf den  erwarteten Wert false gesetzt wurde

Das ist möglich, weil wir die “fake” WordPress Funktionen so gestaltet haben, dass sie sich absichtlich in dieser Art und Weise verhalten, um die Tests zu erleichtern.

Anders als beim Durchführen des Test-Ladens von WordPress gehen wir jetzt davon aus, dass die echten WordPress Funktionen gut funktionieren. Jetzt testen wir, ob unsere Funktionen gut dem den WordPress Funktionen interagieren. Das ist ja genau das, was wir tun wollten! Außerdem ersparen wir uns die Last des WordPress-Ladens.

Es scheint also eine Win-Win-Situation zu sein … und es ist zu gut als dass es etwas wäre, dass ich selbst entwickelt hätte.

Don’t “stub” your toe

Die oben angewandte Technik ist nichts Neues, sie ist eigentlich schon so alt wie Unit Tests selbst. Die “Fake” Version von Drittanbieter-Code (in unserem Fall WordPress), geschrieben nur um benutzerdefinierten Code zu testen, wird “Stub” genannt.

Wie wir gesehen haben, sind Stubs eine großartige Hilfe, um Code zu testen. Aber sie haben ein Problem: Sie sind auch Code. Und wie jedes Stück Code müssen auch sie geschrieben und gewartet werden und sie könnten haben Bugs (Nur Code, der nicht exisitert, hat keine Bugs) haben.

Darum ist es sehr wichtig, dass du Stubs so kurz und einfach wie möglich hältst, wenn du sie schreibst. Indem du ihre Länge und Komplexität auf ihr Minimum reduzierst, gehst du den einzigen Weg, um den Wartungsaufwand und die Zahl der Bugs zu minimieren.

Wenn der einzige Weg, Code zu testen, das Schreiben langer und komplexer Stubs ist, dann ist etwas Falsch – Entweder im Test oder im Code, oder vielleicht ist es in diesem Fall einfach besser, den echten Code zu nutzen und nicht den Stubs Code.

Unter Umständen werden dir einige erfahrene Entwickler, denen du vertraust, sagen, dass du niemals Stubs schreiben und immer den echten Code nutzen sollst, und dass das deine Unit Tests nicht weniger uneinheitlich macht. Wie ich bereits sagte, die Debatte “Das ist ein Unit Test” vs. “Das ist kein Unit Test” kann für manche interessant sein, aber nicht für diesen Artikel.

Die Sache ist die, dass du, sobald du mit WordPress arbeitest, einfach aufgrund des Wesens von WordPress eher vermeiden solltest, die gesamte WordPress Umgebung mit allen Nebeneffekten und Anstrengungen, die es mit sich bringt (in Sachen Konfigurationsaufwand und Performance) zu laden, wenn du Unit Tests schreibst, insbesondere während der Entwicklung. Für mich ist es keine Sache der Correctness oder Purity von Unit Tests, sondern einfach eine Sache der Zweckmäßigkeit.

Wenn eine Klasse von 200 Zeilen Code keinen WordPress Code nutzt, außer einen Aufruf zu  trailingslashit und um es richtig zu testen muss ich 20 Tests schreiben, dann hab ich zwei Möglichkeiten zur Auswahl:

  1. Eine ad-hoc WordPress Installation inklusive Datenbank und Konfiguration vorbereiten und jedes Mal, wenn ich meine Testfolge laufen lasse, die WordPress Umgebung 20 Mal laden lassen, ein Mal je Test
  2. Oder einmal einen Stub von einer Zeile schreiben.

Für mich ist es viel offensichtlicher, was zwischen beiden Optionen die überzeugendere ist. Vielleicht war es ein unfairer Vergleich, weil man in der echten Welt bei WordPress Plugins mehr als nur eine WordPress Funktion nutzt, aber jedes Mal WordPrss zu laden wird niemals überzeugender werden, vor allem, wenn du bedenkst, dass:

  • Je größer das Plugin, desto häufiger würde WordPress mit allen dazugehörigen Performance Schwierigkeiten geladen werden
  • (Spoiler) es gibt Möglichkeiten, Stubs nicht mit Hand schreiben zu müssen

Versteh mich nicht falsch. An einem gewissen Punkt ist es wichtig, Tests durchzuführen, die auch WordPress laden, das ist wesentlich für Plugin mit einer großen UI Integration, aber die Schnelligkeit und die Einfachheit, Tests ohne das Laden von WordPress durchzuführen, gewinnt in meinen Augen immer aufgrund der gesteigterten Produktivität und der verringerten Frustration. Außerdem ist meine Erfahrung, dass, wenn Entwickler das Testen von Plugins ohne das Laden von WordPress lernen, die Anzahl des getesteten Codes, den sie schreiben, exponentiell wächst.

Zurück zu Stubs: Sie sind großartig, aber schwer.

Sie müssen geschrieben werden, sie müssen gewartet werden, sie brauchen besondere Aufmerksamkeit beim Entwickeln, weil ein schlecht geschriebener Stub dazu führen kann, dass eine Anwendung unter schlechten Vorraussetzungen getestet wird und dann komplett failed, wenn sie mit dem echten Code läuft  (und ich habe noch nie jemanden getroffen, der die in Tests verwendeten Stubs getesten hat).

All das sind Argumente für der “Nutze keine Stubs” Menschen und all das sind Gründe, die Zahlen der Stubs zu limiteren und sie so einfach wie möglich zu machen.

Let’s OOP for the best

Die Isolation von Drittanbieter-Code ist nicht das einzige Issue, wenn man sich mit Unit Tests beschäftigt. Häufig gibt es Code, der IO Operationen auf einem Filesystem, Datenbanken, externen Services und so weiter durchführt. Diese Operationen sind wegen zahlreicher Gründe (Nichtverfügbarkeit des Ziels,  schwer zu reproduzierender Status, Performance Issues und so weiter) furchtbar schwer zu testen.

Selbst Entwickler, die normalerweise gegen “Fake Code” Techniken sind, sind dafür, die Tests, in welcher diese Art von Operationen durchgeführt werden, zumindest als Integration oder Systemtests (und nicht unit) zu bezeichnen, und jeder stimmt darin überein, Real Production Code in solchen Fällen in Unit Tests nicht durchzuführen.

Außerdem gibt es andere Arten von Operationen, die nur bei nicht-festgelegten Situationen gelten (zum Beispiel zu einer bestimmten Tageszeit), und es ist wichtig, dass es möglich ist, diese Operationen in Unit Tests in einer festgelegten Art und Weise zu testen.

Diese Themen sind so alt wie Software Entwicklung selbst, und die Art und Weise, wie Entwickler sie in der Vergangenheit gelöst (oder es zumindest versucht) haben, bedeutete entweder den Einbezug von Special Flags in den Production Code, um Testen zu ermöglichen oder den Gebrauch von Stubs.

Objekt-orientiertes Programmieren (OOP), wenn richtig durchgeführt, bietet einen effektiven Ansatz, um dieses Issue zu behandeln.

Wenn es ein Objekt gibt, was furchtbar zu testen ist (wegen IO Operationen oder ihrer nicht-deterministischen Art), kann sein Verhalten hinter einem Interface “versteckt” werden, sodass es möglich ist, eine andere, vereinfachte Implementierung der gleichen Interface nur für Tests zu schreiben.

Wenn OOP richtig durchgeführt wird, werden sich andere Softwareeinheiten auf das Interface und nicht die Implementierung verlassen, sodass es uns ermöglicht wir Tests auf dieser anderen vereinfachten Implementierung laufen zu lassen und andere Einheiten würden es nicht einmal bemerken.

Was in dem obigen Absatz beschrieben ist, ist sehr ähnlich zu dem bereits vorgestellten “Stub” Konzept und eigentlich ist die vereinfachte Implementierung, wie sie hier beschrieben ist, auf jeden Fall ein Stub-Objekt. Allerdings beruhte die Stubs-Funktion weiter oben auf der Tatsache, dass die ursprüngliche Funktion nicht verfügbar war.

Für Stub-Objekte ist das Laden der ursprünglichen Implementierung kein Problem, da der Stub eine alternative Implementierung ist, die wir anstelle des Originals verwenden können.

Ein bisschen Code, um die Gewohnheit nicht zu verlieren:
interface Clock {

    public function hours(): int;
    public function minutes(): int;
    public function seconds(): int;
}

final class SystemClock implements Clock {

    public function hours(): int {
        return (int) ( new Datetime() )->format( 'G' );
    }

    public function minutes(): int {
        return (int) ( new Datetime() )->format( 'i' );
    }

    public function seconds(): int {
        return (int) ( new Datetime() )->format( 's' );
    }
}

function maybeGong( Clock $clock ) {

    if ( $clock->minutes() === 0 && $clock->seconds() === 0 ) {
        $h     = $clock->hours();
        $gongs = $h > 0 && $h <= 12 ? $h : abs( $h - 12 );
        print rtrim( str_repeat( 'GONG ', $gongs ) );
    }
}
Und der Test:
<?php
require_once __DIR__.'/test-framework-in-a-tweet.php';
require_once '/path/to/clock-and-gong.php'; // The code above

class ClockStub implements Clock {

    private $hours;
    private $minutes;

    public function __construct( int $hours, int $minutes ) {
        $this->hours   = $hours;
        $this->minutes = $minutes;
    }

    public function hours(): int {
        return $this->hours;
    }

    public function minutes(): int {
        return $this->minutes;
    }

    public function seconds(): int {
        return 0;
    }
}


it( 'should gong 12 times at midnight.', function () {
    ob_start();
    maybeGong( new ClockStub( 0, 0 ) );
    return ob_get_clean() === "GONG GONG GONG GONG GONG GONG GONG GONG GONG GONG GONG GONG";
} );


it( "should gong 3 times at 3 o'clock.", function () {
    ob_start();
    maybeGong( new ClockStub( 3, 0 ) );
    return ob_get_clean() === "GONG GONG GONG";
} );


it( 'should not gong at 10 past 3.', function () {
    ob_start();
    maybeGong( new ClockStub( 3, 10 ) );
    return ob_get_clean() === "";
} );

Schlussbetrachtungen

Im Code oben war es uns möglich, das Verhalten der maybeGong zu testen, auch wenn es eigentlich ein klassisches Beispiel nicht-deterministischen Verhaltens ist.

Es war möglich, weil die maybeGong Funktion als Argument eine Oberfläche akzeptiert, die wir durch eine deterministische Implementierung ersetzen konnten. Wenn die Funktion eine Instanz als SystemClock  innerhalb des Funktionsblocks instanziiert hätte, wäre das Testen beinahe unmöglich gewesen, es sei denn, man wollte darauf warten, dass die genau zweite SystemClock das nächste o’clock erreicht hätte, um den Test zu starten.

Was wir hieraus lernen ist, dass es nötig ist, dass der Code, den wir testen möchten, nicht mit spezifischen Implementierungen gekoppelt ist, außer durch ihre Interfaces. Und das ist einer der Hauptgründe dafür, warum mancher Code als “testbar” und anderer als “nicht testbar” definiert wird. Aber das ist für ein anderes Weihnachten, vielleicht.

Diese neue Art des OOP Stub löst eigentliche einige Probleme, aber andere Stub-Probleme sind immer noch vorhanden: Im letzten Beispiel – auch wenn es sehr trivial ist – machte das Schreiben des Tests einen Stub von 22 Zeilen Code notwendig, die jetzt gewartet werden müssen. Zum Beispiel, wenn sich in der nächsten Version der Software das Clock Interface ändert, muss sich der Stub ebenfalls ändern.

Gibt es einen anderen effektiven Weg? Ist da irgendetwas, was wir tun können, um den Wartungsaufwand der ad-hoc Stubs zu reduzieren? Und wenn Stubs eine Objekt-Sache sind, wie gehen wir dann mit den aberhunderten von WordPress Funktionen um, wenn WordPress nicht geladen wird?