WordPress Plugins Unit Tests … ohne das Laden von WordPress – Teil 2

Das ist der zweite Teile meines umfassenden Beispiels in Unit Tests von WordPress Code ohne das Laden der gesamten WordPress Umgebung.

… Vorher …

Im ersten Teil dieser zweiteiligen Serie haben wir festgestellt, dass Unit Tests im Grunde genommen ein ganz einfaches Konzept haben. Dieses benötigt keine komplexen Frameworks oder aufgeblähte Bibliotheken, es geht nur darum, bestimmten Code auszuführen und Feedback zu bekommen. Wir haben sogar herausgefunden, wie eine auf tweetgröße beschränkte Funktion genutzt werden kann, um die Unit Tests zu schreiben, die wir brauchen könnten.

Danach haben wir sowohl den Nutzen als auch die Probleme analysiert, die beim Durchlauf von Unit Tests für WordPress entstehen, wenn WordPress nicht geladen wird. Und dann haben wir uns angeschaut, wie uns Stubs ermöglichen das zu tun. Aber mit der Verwendung von Stubs stehen wir vor ein paar Problemen: Sie müssen geschrieben werden, sie bieten Bugs eine Angriffsfläche, sie müssen gewartet werden.

In diesem zweiten Teil werden wir uns einen anderen Weg und Tools anschauen, um diese Issues zu umgehen.

Wenn du den ersten Artikel am 09. Dezember noch nicht gelesen hast, dann hole das bitte an dieser Stelle nach. Wir werden uns nämlich auf die beschriebenen Konzepte und Code Beispiele beziehen.

Mock rund um die Uhr

Im ersten Teil haben wir gesagt, dass das, was zählt, wenn wir Plugins testen, nicht die Überprüfung ist, ob der WordPress Code funktioniert (wir gehen davon aus, dass er funktioniert). Stattdessen testen wir, ob der Custom Code die WordPress Funktionen auf die richtige Art und Weise nutzt.

Zu diesem Zweck haben wir uns einige Beispiele angeschaut, bei denen wir mithilfe von Stubs WordPress Plugin Code testen konnten, ohne WordPress zu laden.

Aber … Haben wir eigentlich getestet, ob die WordPress Funktionen richtig genutzt wurden?

Zum Beispiel: Haben wir getestet, was passiert, wenn sie mehr als einmal oder gar nicht aufgerufen werden? War es uns möglich die Argumente zu testen, die an diese WordPress Funktionen weitergegeben wurden?

Der Stub, den wir geschrieben haben, war sehr einfach (absichtlich) und nicht fähig, diese Art ungeeigneter Verwendungen von WordPress Code abzufangen. Und das wiederum bedeutet, dass, wenn unser Code Live gehen und dann WordPress geladen werden würde, die Gefahr von Fehlern besteht.

Was wir brauchen würden ist ein Stub, der den Fehlertest macht, ob die erwartete Funktion eigentlich gar nicht aufgerufen wird, oder mit der falschen Anzahl aufgerufen wird oder mit den falschen Argumenten aufgerufen wird. Das würde (bis zu einem gewissen Grad) garantieren, dass beim Bestehen des Tests dann auch das richtige Ding funktioniert, wenn wir den Stub ersetzen.

Ich habe soeben ein Mock Object beschrieben. Kurz gesagt: Ein Objekt, dass die gleichen Interfaces des ursprünglichen Objekts implemeniert. Es ermöglicht zu testen, ob die Methodenaufrufe, die es erhält, konform sind mit einer Reihe niedergeschriebener Erwartungen. Diese wurden vorab aufgeschrieben, um die Konformität mit dem Originalcode zu gewährleisten. Häufig werden Mock Objects nicht im Filesystem gespeichert. Sie sind “virtuelle” Objekte, die zum Zeitpunkt der Ausführung mit der Hilfe von Libraries auf der Basis gegebener Erwartungen erstellt werden.

Im ersten Teil des Artikels haben wir das Beispiel eines Clock Objektes geschrieben und wir sahen, wie wir einen Stub nutzten, um es zu testen. Lasst uns jetzt anschauen, wie ein Mock Object stattdessen genutzt werden könnte:

<?php
// Mockery is installed via Composer and requires autoload.
require_once __DIR__.'/vendor/autoload.php';
require_once __DIR__.'/test-framework-in-a-tweet.php';
require_once '/path/to/clock-and-gong.php';


it( "should gong 3 times at 3 o'clock.", function () {
  
    $threeOClock = \Mockery::mock('Clock');
    $threeOClock
        ->shouldReceive('hours')
        ->once()->withNoArguments()->andReturn(3);
    $threeOClock
        ->shouldReceive('minutes')
        ->once()->withNoArguments()->andReturn(0);
    $threeOClock
        ->shouldReceive('seconds')
        ->once()->withNoArguments()->andReturn(0);
  
ob_start();
maybeGong( $threeOClock );
$result = ob_get_clean() === "GONG GONG GONG";
  
    \Mockery::close();
  
    return $result;
} );

Eine PHP Library mit dem Namen Mockery wird oben verwendet, um einen Mock der gleichen Clock Interface zu erstellen, die wir im ersten Beitrag gesehen haben.

Das Mockobjekt erledigt den gleichen Job der ClockStub, die wir uns angeschaut haben, aber:

  • Keine Notwendigkeit einer weiteren Klasse in der Codebasis:  Das Mock Object wird just in time im Test selbst erstellt.
  • Der erforderliche Code ist viel weniger (22 Zeilen VS 4) und bietet quasi keine Angriffsfläche für Bugs. In den 4 Zeilen Code, die es benötigt, gehört der gesamte Code zur Mockery Library, welche separat getestet wird.
  • Der Mock ist effektiver als der Stub, weil er nicht nur in einer vorab definierten Art und Weise auf Methodenaufrufe antwortet, sondern ebenfalls fähig ist sicherzustellen, dass die Methoden mit einer bestimmten Zahl und Typen von Argumenten aufgerufen werden.

Ich nutzte Mockery, weil ich seine DSL mag und die Tatsache, dass es mit jedwedem Testframework (sogar mit dem kleinen TestFrameworkInATweet) genutzt werden kann. Aber es ist nicht die einzige PHP Bibliothek, die es ermöglicht Mockobjekte zu erstellen.

PHPUnit, das bekannteste PHP Testframework, hat eine integrierte Mock Library. Phake und AspectMock sind andere Alternativen.

A technische Notiz: Mockery, welches im weiteren Verlauf des Artikels verwendet wird, verlangt, dass Mockery::close() am Ende jeden Tests abgerufen wird. Aus diesem Grund werde ich für den Rests des Artikels auf die folgende Zeile verweisen:

<?php
namespace m;

require __DIR__.'/vendor/autoload.php';
require __DIR__.'/test-framework-in-a-tweet.php';
  
function it($m,$p) { $d=debug_backtrace(0)[0];
    try {
        ob_start();
        \it($m,$p);
        $output = ob_get_clean();
        \Mockery::close();
        echo $output;
    } catch(\Throwable $t) {
        $GLOBALS['e'] = true;
        $msg = $t->getMessage();
        echo "\e[31m✘ It $m\e[0m ";
        echo "FAIL in: {$d['file']} #{$d['line']}. $msg\n";
    }
}

welche die TestFrameworkInATweet vom ersten Beitrag um Mockery Features erweitert. Dadurch können wir jetzt Tests wie folgt schreiben:

<?php
// this file contains the 19 lines in the snippet above
require __DIR__.'/test-framework-in-a-tweet-mockery.php';

m\it( "should work with Mockery", function () {
    $mock = Mockery::mock();
    // this is not satisfied, so this test should fail
    $mock->shouldReceive('test')->once();
    return true;
});

und das Ergebnis des obigen Tests sollte Folgendes sein:

✘ It should work with Mockery, which means this test should fail. FAIL in: /path/to/test-file.php #9. Method test(<Any Arguments>) from Mockery_0 should be called exactly 1 times but called 0 times.

Viel mit Mock in WordPress

Mockery (und andere PHP Mocking Libraries) erlauben Objekte zu mocken, und das ist eine großartige Hilfe, wenn wir WordPress Plugins testen wollen, ohne WordPress zu laden.

Das letzte Mal, als ich es nachgeschaut habe, waren rund 51% der mehr als 330000 Zeilen von WordPress PHP Code (und Bibliotheken, die er einbettet), in 368 Klassen (und 6 Interfaces) geschrieben.

Lasst uns ein Beispiel anschauen, wie wir mit Mockery und unserem soeben verbesserten Testframework WordPress Code testen können, der Objekte nutzt.

Zunächst ein bisschen WordPress Code:

<?php
namespace MyCompany\MyPlugin;

class ProductsQuery {

    private $query;
    private $paged = 0;

    public function __construct( \WP_Query $query = NULL ) {
        $this->query = $query ? : new \WP_Query();
    }

    public function next_products(): array {
        $this->paged ++;
        $this->query->query( [
            'post_type'      => 'product',
            'posts_per_page' => 10,
            'orderby'        => 'title',
            'order'          => 'ASC',
        ] );
        if ( ! $this->query->have_posts() ) {
            return [];
        }

        return $this->query->posts;
    }
}

Das ist eine Klasse, die WP_Query verpackt und bestimmte Argumente weitergibt, um einen “Produkt” Post Type abzufragen.

In der Klasse geht es nur darum, WP_Query Methoden aufzurufen. Und wir möchten nicht WP_Query testen, sondern wir wollen testen, ob WP_Query Methoden richtig aufgerufen werden und ob das Ergebnis des Aufrufens unserer Klassenmethoden den Erwartungen entspricht.

Also lasst es uns tun:

<?php
require __DIR__.'/test-framework-in-a-tweet-mockery.php';
require '/path/to/MyCompany/MyPlugin.php/ProductsQuery.php';


m\it( "should return no products if no products.", function () {
  
    $query_mock = Mockery::mock( 'WP_Query' );
    $query_mock
        ->shouldReceive( 'query' )
        ->once()
        ->with( \Mockery::type( 'array' ) );
    $query_mock
        ->shouldReceive( 'have_posts' )
        ->once()
        ->withNoArgs()
        ->andReturn( false );

    $query = new MyCompany\MyPlugin\ProductsQuery( $query_mock );

    return $query->next_products() === [];
} );


m\it( "should get first 10 products.", function () {

    $ten_posts = array_map(
        function() { return Mockery::mock('WP_Post'); },
        range(0, 9)
    );

    $query_mock = Mockery::mock( 'WP_Query' );
    $query_mock
        ->shouldReceive( 'query' )
        ->once()
        ->withArgs( function ( array $args ) {
            return
                ($args[ 'post_type' ] ?? '') === 'product'
                && ($args[ 'posts_per_page' ] ?? '') === 10;
            } )
            ->andSet( 'posts', $ten_posts );
    $query_mock
        ->shouldReceive( 'have_posts' )
        ->once()
        ->withNoArgs()
        ->andReturn( true );

    $query = new MyCompany\MyPlugin\ProductsQuery( $query_mock );

    return $query->next_products() === $ten_posts;
} );

Dank Mockery testet der Code oben, ob unsere Klasse Werte wiedergibt, die zum dem passen, was WP_Query wiedergibt, und ob die WP_Query Methoden genau so häufig aufgerufen werden, wie die erwartete Anzahl ist. Und das ganze ohne das Laden von WordPress.

Ich denke, du solltest dir einmal die Mockery Dokumentation anschauen, weil sie viele Features hat, die ich in diesem Artikel nicht alle beschreiben werde.

Aber nur so nebenbei … Das ist ziemlich cool, oder?

Ja, aber … wenn es wahr ist, dass WordPress einige hunderte von Klassen hat, hat es mehr als 2900 Funktionen und ohne einen effektiven Weg, Mocks/Stubs durchzuführen, dann gibt es auch keinen Weg, WordPress Plugincode ohne das Laden von WordPress zu testen.

Der clevere Monkey

Mock Objects, funktionieren für … Objekte, aber wir WordPress Entwickler brauchen etwas Ähnliches für Funktionen.

Im ersten Teil des Artikels haben wir gesehen, dass es sehr leicht ist Stubs zu schreiben, wenn WordPress Funktionen nicht definiert sind. Allerdings wollen wir, wenn wir in der echten Welt Tests schreiben, verschiedene Stubs auf der Basis von dem schreiben, was wir testen wollen. Aber PHP erlaubt es nicht, Funktionen umzudefinieren.

Es erscheint logisch, dass wir Folgendes brauchen:

  1. Ein Weg, um die gleichen Konzepte von Object-Mocks auf Funktionen anzuwenden.
  2. Ein Weg, um Funktionen umzudefinieren.

Der erste Punkt ist ziemlich einfach zu realisieren. Eine Funktion unterscheidet sich nicht wesentlich von einem Objekt mit einer einzigen Methode. Also brauchen wir nur irgendwelchen Code, der den Namen der Funktion erhält, die wir ersetzen möchten, dafür ein Objekt mit einer einzigen Methode erstellt und dann Mockery Erwartungen zu diesem Objekt hinzufügt.

Der zweite Punkt ist um einiges schwerer. Seit 2010 ist eine Open Source Bibliothek mit dem Namen Patchwork verfügbar, die es ermöglicht PHP Funktionen beim Ausführungszeitpunkt umzudefinieren (Laufzeit-Re-Definition von Funktionen wird manchmal als “monkey patching” bezeichnet).

Auch wenn PHP es nicht erlaubt, Monkey-Patches von Quellcode-Funktionen durchzuführen, verwendet Patchwork einen Trick (ich spare die technischen Details), der es ermöglicht, PHP-Funktionen neu zu definieren.

Also haben wir jetzt alle Zutaten, die helfen könnten, WordPress Funktionen ohne das Laden von WordPress zu testen – wir müssen sie nur noch “zusammenmixen”.

Stell dir vor, das hat schon jemand gemacht.

Vor nunmehr drei Jahren begann ich Brain Monkey zu entwickeln, was als einzelne Klasse von etwa 100 Zeilen Code begann. Brain Monkey tut im Prinzip genau das, was ich oben beschrieben habe: Es bietet eine “Brücke” zwischen der Funktions-Neudefinierung von Patchwork und der Mock-Erstellung von Mockery.

Die Brain Monkey Dokumentation sagt uns, dass wir ein bisschen konfigurieren müssen, bevor wir es nutzen können: Wir müssen Brain\Monkey\setUp() vor jedem Test und Brain\Monkey\tearDown() nach jedem Test abrufen.

Brain Monkey nutzt Mockery, also ruft seine tearDown() Funktion bereits Mockery::close().

Weil ich dir zeigen will, wie du Brain Monkey nutzen kannst, werde ich einige Zeilen Code schreiben, um unser Mikrotestframework um Brain Monkey Möglichkeiten zu erweitern:

<?php
namespace bm;

require_once __DIR__.'/vendor/autoload.php';
require_once __DIR__.'/test-framework-in-a-tweet.php';
  
function it($m,$p) { $d=debug_backtrace(0)[0];
    try {
        \Brain\Monkey\setUp();
        ob_start();
        \it($m,$p);
        $output = ob_get_clean();
        \Brain\Monkey\tearDown();
        echo $output;
    } catch(\Throwable $t) {
        $GLOBALS['e'] = true;
        $msg = $t->getMessage();
        echo "\e[31m✘ It $m\e[0m";
        echo "FAIL in: {$d['file']} #{$d['line']}. $msg\n";
    }
}

Lasst uns ein Beispiel von echtem WordPress Code anschauen, das wir gleich testen werden. Es ist eine Klasse, die die Registrierung und das Rendern eines Shortcodes verwaltet:

<?php
namespace MyCompany\MyPlugin;

class LastPostShortcode {

    const DEFAULTS = [
        'title'     => TRUE,
        'date'      => TRUE,
        'content'   => FALSE,
        'post_type' => 'post',
    ];

    public function register() {
        add_shortcode( 'last_post', [ $this, 'render' ] );
    }

    public function render( $args ) {
      
        $defaults = self::DEFAULTS;
        $atts = shortcode_atts( $defaults, $args, 'last_post' );

        $query = new \WP_Query([
            'post_type'      => $atts[ 'post_type' ],
            'posts_per_page' => 1,
            'orderby'        => 'date',
            'order'          => 'DESC',
        ]);
      
        if ( ! $query->have_posts() ) {
            return;
        }

        $query->the_post();

        printf( '<div class="last_post" id="%s">', get_the_ID() );
        if ( $atts[ 'date' ] ) {
            printf('<p class="date">%s</p>', get_the_date() );
        }
        if ( $atts[ 'date' ] ) {
            printf('<h1>%s</h1>', esc_html( get_the_title() ) );
        }
        if ( $atts[ 'content' ] ) {
            the_content();
        }
        print '</div>';
      
        wp_reset_postdata();
    }
}

So wie in jedem Durchschnitts-WordPress-Code gibt es auch hier viele Funktionsaufrufe.

Für manche von ihnen, zum Beispiel wp_reset_postdata, sind wir hinsichtlich der Tatsache an der Funktion interessiert, dass sie definiert ist. Für solche Funktionen würde es etwas wie das hier sein, wenn wir den Stub “mit Hand” schreiben würden:

function wp_reset_postdata() {}

Gerade genug, um keinen Fatal Error aufgrund einer undefinierten Funktion herbeizuführen.

Für andere Funktionen wie esc_html, würden wir zu Testzwecken nur das erste Argument wiedergeben, das sie unverändert erhalten. Die Tatsache dass sich diese Funktionen tatsächlich den Werten entziehen ist nichts, was wir in unserem Plugin testen sollten (wir gehen davon aus, dass sie  funktionieren). Für diese Funktionen würde der “mit Hand” Stub so aussehen:

function esc_html( $arg ) { return $arg; }

Dann haben wir Funktionen, bei denen wir nicht wirklich kontrollieren müssen, wie sie aufgerufen werden (oder wie oft), sondern wir müssen den Wiedergabewert kontrollieren. Zum Beispiel:


function get_the_ID() { return 123; }
function get_the_title() { return 'Test'; }

Und schließlich sind da noch Funktionen, für die wir die volle Kontrolle brauchen: Ob, wann, wie und wie oft sie aufgerufen werden.

Zum Beispiel wollen wir für  add_shortcode wahrscheinlich testen, ob es tatsächlich einmal gerufen wird, dass die Argumente die richtigen sind und dass sie in der richtigen Reihenfolge aufgerufen werden. Das Gleiche für shortcode_atts, weil wir überprüfen wollen, ob wir Argumente in der richtigen Reihenfolge übergeben und dass das dritte Argument dem add_shortcode des ersten Arguments entspricht.

Wir werden bald sehen, wie uns Brain Monkey helfen wird, das zu erhalten was wir wollen, ohne irgendeinen Stub mit Hand zu schreiben.

Bezogen auf Objekte nutzt die Klasse, die wir testen werden, eine einzige Instanz der WP_Query, aber diesmal wird WP_Query direkt in der Rendermethode instanziiert.

Normalerweise würde ich sagen, dass ist eine schlechte Handhabung. Das Problem hier ist, dass WordPress Klassen nicht wirklich guten OOP Prinzipien folgen. Zum Beispiel akzeptiert WP_Query Query-Argumente im Constructor und eine Datenbankabfrage wird ausgeführt, sobald ein Objekt erstellt wurde. Aus diesem Grund ist es üblich WP_Query Instanzen erstellen zu lassen, direkt bevor sie genutzt werden.

Wie können wir dieses Ding wohl testen? Mockery hat ein Feature für uns. Dank seinem overload Feature ermöglicht uns Mockery Aufrufe nach new keyword abzuhören und die instanziierte Klasse durch ein Mock Object zu ersetzen.

Lasst es uns ausprobieren:

<?php
// Load Patchwork as early as possible
require_once __DIR__.'/vendor/antecedent/patchwork/Patchwork.php';
require_once __DIR__.'/test-framework-in-a-tweet-brain-monkey.php';
require_once '/path/to/MyCompany/MyPlugin/LastPostShortcode.php'

use Brain\Monkey;
use MyCompany\MyPlugin\LastPostShortcode;


bm\it( 'should render shortcode.', function () {

    $args = [ 'title' => FALSE, 'content' => TRUE, ];
    $all_args = array_merge( LastPostShortcode::DEFAULTS, $args );

    // Full control on `shortcode_atts`
    Monkey\Functions\expect( 'shortcode_atts' )
        ->once()
        ->with( LastPostShortcode::DEFAULTS, $args, 'last_post' )
        ->andReturn( $all_args );

    // We want these be defined and know their return value
    Monkey\Functions\stubs([
        'get_the_ID'        => 123,
        'get_the_date'      => '15/12/2017',
        'get_the_title'     => 'Test Title',
        'esc_html'          => null,
        'the_content'       => function () {
             echo '<p>The content!</p>';
         },
        'wp_reset_postdata' => TRUE
    ]);

    // Overload WP_Query
    $query_mock = Mockery::mock( 'overload:WP_Query' );
    $query_mock->shouldReceive( 'have_posts' )
        ->once()->andReturn( TRUE );
    $query_mock->shouldReceive( 'the_post' )
        ->once();

    // Perform the method!
    $sh = new LastPostShortcode();
    ob_start();
    $sh->render( $args );
    $rendered = ob_get_clean();

    // Let's test the rendered string is what we expect:
    $has_id       = strpos( $rendered, 'id="123"' );
    $has_date     = strpos( $rendered, '15/12/2017' );
    $has_no_title = strpos( $rendered, '<h1>' ) === FALSE;
    $has_content  = strpos( $rendered, '<p>The content!</p>' );

    return $has_id && $has_date && $has_no_title && $has_content;
} );

Wir haben eine echte WordPress Plugin Klasse erfolgreich getestet, ohne WordPress zu laden.

Natürlich ist der Test sehr groß!

Die begrenzten Features des Mikrotestframeworks helfen hier nicht.

Wenn wir ein fortgeschritteneres Framework hätten, könnten wir einen viel kleineren Test haben: Wir würden alle Funktionen, die zu einer separaten Testklassenmethode stubben, extrahieren.  Das gleiche gilt für Abfrage-Overloading. Anstatt alle Aussagen auf einmal durchzuführen, hätten wir auch einfach unterschiedliche Tests nutzen können.

Außerdem müssen wir den Ausgabepuffer manuell verwalten, während fortgeschrittene Frameworks dieses Feature eingebettet haben.

Um dir eine Vorstellung zu geben – in PHPUnit könnte ein Test für die gleiche Klasse so aussehen:


public function test_post_content_is_renderered_if_enabled() {

    $args = [ 'content' => true ];
    
    $this->expect_shortcode_atts_for( $args )
    $this->stub_functions();
    $this->overload_query();
    
    $this->expectOutputRegex('~<p>The content!</p>~');
    
    $sh = new LastPostShortcode();
    $sh->render( $args );
}

Magst du Brain Monkey bis hier? Sehr gut! Es kann noch mehr!

Süchtig nach Testen

Mit Mockery, was alle Methoden liefert, um Objekte zu mocken und zu stubben und mit Brain Monkey, was diese Features zu Funktionen bringt haben wir alles was wir brauchen, um WordPress Plugins zu testen ohne WordPress zu laden.

Allerdings gibt es einige Funktionen, die wir überall in WordPress Plugins finden, und sie jedes mal zu mocken ist ziemlich mühsam und furchtbar. Ich rede über die Plugin API Funktionen.

Stelle sicher, dass eine Aktion oder ein Filter hinzugefügt wurde und mit dem callable, testen, dass eine Aktion ausgelöst worden ist und mit welchen Argumenten, oder dass ein Filter angewendet worden ist und vielleicht darauf antworten … Das sind alles Operationen, die uns das Gefühl geben, echte WordPress Plugins sehr häufig testen zu wollen.

Natürlich können wir die Brain Monkey Functions\expect nutzen, um Funktionsstubs add_action, add_filter, do_action, apply_filter zu erstellen und all die anderen Funktionen der API’s in Plugins, aber das würde sehr viel Bootstrap Code für jeden Test bedeuten und den Nutzen von Tests ohne das Laden von WordPress reduzieren oder sogar auslöschen.

Zum Glück hat Brain Monkey eine API designed, nur um WordPress Plugins API zu testen.

Die ersten guten Neuigkeiten sind, dass Brain Monkey bereits alle Funktionen der Plugins-API in einer Art und Weise definiert, die zu 100% kompatibel mit dem echten Code von WordPress ist.

Das bedeutet, dass wenn der Code der Plugins, den wir testen wollen, viele Funktionen der Plugins-API nutzt, könnten wir testen, ohne überhaupt irgendetwas zu mocken oder zu stubben.

Zum Beispiel:

<?php
namespace MyCompany\MyPlugin;
  
function create_initializer() {
  
    if ( ! did_action( 'init' ) ) {
        return null;
    }
  
    do_action( 'my_plugin_init' );
  
    $init_class = apply_filters(
        'my_plugin_init_class',
        Initializer::class
    );
    $initializer = new $init_class();
  
    add_action( 'template_redirect', [ $initializer, 'init' ] );
  
    return $initializer;
}

Das sieht aus wie echter WordPress Code.

Dank Brain Monkes könnten wir etwas wie das hier tun:

<?php
require_once __DIR__.'/vendor/antecedent/patchwork/Patchwork.php';
require_once __DIR__.'/test-framework-in-a-tweet-brain-monkey.php';
require_once '/path/to/MyCompany/MyPlugin/functions.php';
require_once '/path/to/MyCompany/MyPlugin/Initializer.php';
 
use MyCompany\MyPlugin;


bm\it(
    'should not initialize anything if WP is not initialized.',
    function () {
        return MyPlugin\create_initializer() === null;
    }
);


bm\it(
    'should instantiate Initializer if WordPress is initialized.',
    function () {
        do_action( 'init' );
        $initializer = MyPlugin\create_initializer();
        return $initializer instanceof MyPlugin\Initializer;
    }
);

Wir führen unsere Tests genauso als ob WordPress geladen würde, ohne jeden Mock oder Stub, und alles funktioniert genauso wie erwartet!

Die Funktion im Test nutzt vier Funktionen der Plugins-API (did_action, do_action, apply_filters, und add_action) und wir haben sogar do_action genutzt, es testet Code … und alles hat einfach nur funktioniert.

Selbst wenn das schon großartig ist (Entschuldigung, ich bin voreingenommen) – die Tatsache, dass wir einfach Erwartungen auf diese Hooks setzen oder sogar auf Filter anworten können, ist sogar noch besser.

Ein paar andere Tests, die wir schreiben können:

use Brain\Monkey\Action;
use Brain\Monkey\Filters;


bm\it(
    'should not fire my_plugin_init if WP is not initialized.',
    function () {
        Actions\expectDone( 'my_plugin_init' )
            ->never();

        MyPlugin\create_initializer();

        return true;
    }
);


bm\it(
    'should fire my_plugin_init once if WP is initialized.',
    function () {

        Actions\expectDone( 'my_plugin_init' )
            ->once()
            ->withNoArgs();

        do_action( 'init' );

        MyPlugin\create_initializer();

        return true;
    }
);


bm\it(
    'should add hook initializer method to template redirect',
    function () {
        Actions\expectAdded( 'template_redirect' )
            ->once()
            ->withArgs( function($cb) {
                return
                    is_a( $cb[0], MyPlugin\Initializer::class )
                    && $cb[1] === 'init';
            } );

        do_action( 'init' );

        MyPlugin\create_initializer();

        return true;
    }
);


bm\it(
    'should use a different initializer class if filtered.',
    function () {

        $mock       = \Mockery::mock( MyPlugin\Initializer::class );
        $mock_class = get_class( $mock );

        Filters\expectApplied( 'my_plugin_init_class' )
            ->once()
            ->with( MyPlugin\Initializer::class )
            ->andReturn( $mock_class );

        do_action( 'init' );

        return is_a( MyPlugin\create_initializer(), $mock_class );
    }
);

In kurz: Wir werden alle Plugins-API-Funktionen unter höchstem Kontroll-Level testen können, und das mit einer Syntax die sehr lesbar und konsistent mit derjenigen ist, die wir nutzen, um Mock Objekte mit Mockery zu erstellen.

Wenn du einen Blick auf die Brain Monkey Dokumentation zum Testen von added hooks und fired hooks wirfst, findest du weitere Features, um deine Tests weiter zu stärken.

Das ist noch mehr …

Bevor ich diesen Artikel beende, möchte ich noch sagen, dass Plugins-API-Funktionen nicht die einzigen Funktionen sind, die bereits von Brain Monkey für dich gemocked sind.

Wenn dein Code irgendeiner der folgenden Funktionen nutzt:

  • __return_true
  • __return_false
  • __return_null
  • __return_empty_array
  • __return_empty_string
  • __return_zero
  • trailingslashit
  • untrailingslashit

Du musst sie nicht mocken, oder irgendetwas … wie werden genauso arbeiten wie im WordPress Kontext.

In kurz: Dank Mockery und Brain Monkey haben wir die Möglichkeit, Unit Tests für unser Plugin zu schreiben, ohne WordPress in irgendeiner Art und Weise zu laden und es ist sehr leicht Prozeduren und und sehr wartbare Tests zu starten.

Hast du Fragen? Feedback? Denkst du, ich habe etwas vergessen? Denkst du, ich liege mit irgendetwas komplett falsch oder sonst irgendwas? Der Kommentarbereich unten ist da, um Feedback zu geben, und wir hören zu …

PS: Brain Monkey ist Open Source und sehr offen für Bearbeitungen.