Hooks Callback testen

34

Ich entwickle ein Plugin mit TDD und eine Sache, die ich nicht testen kann, sind ... Hooks.

Ich meine OK, ich kann Hook-Callback testen, aber wie kann ich testen, ob ein Hook tatsächlich ausgelöst wird (sowohl benutzerdefinierte Hooks als auch WordPress-Standard-Hooks)? Ich gehe davon aus, dass ein bisschen Spott hilft, aber ich kann einfach nicht herausfinden, was mir fehlt.

Ich habe die Testsuite mit WP-CLI installiert. Laut dieser Antwort sollte der initHaken auslösen, doch ... tut es nicht; Der Code funktioniert auch in WordPress.

Nach meinem Verständnis wird der Bootstrap zuletzt geladen, so dass es irgendwie sinnvoll ist, init nicht auszulösen. Die Frage bleibt also: Wie zum Teufel soll ich testen, ob Hooks ausgelöst werden?

Vielen Dank!

Die Bootstrap-Datei sieht folgendermaßen aus:

$_tests_dir = getenv('WP_TESTS_DIR');
if ( !$_tests_dir ) $_tests_dir = '/tmp/wordpress-tests-lib';

require_once $_tests_dir . '/includes/functions.php';

function _manually_load_plugin() {
  require dirname( __FILE__ ) . '/../includes/RegisterCustomPostType.php';
}
tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' );

require $_tests_dir . '/includes/bootstrap.php';

Die getestete Datei sieht folgendermaßen aus:

class RegisterCustomPostType {
  function __construct()
  {
    add_action( 'init', array( $this, 'register_post_type' ) );
  }

  public function register_post_type()
  {
    register_post_type( 'foo' );
  }
}

Und der Test selbst:

class CustomPostTypes extends WP_UnitTestCase {
  function test_custom_post_type_creation()
  {
    $this->assertTrue( post_type_exists( 'foo' ) );
  }
}

Vielen Dank!

Ionut Staicu
quelle
Können Sie im laufenden Betrieb phpunitfehlgeschlagene oder bestandene Tests sehen? Hast du installiert bin/install-wp-tests.sh?
Sven
Ich denke, dass ein Teil des Problems ist, dass vielleicht RegisterCustomPostType::__construct()nie aufgerufen wird, wenn das Plugin für die Tests geladen wird. Es ist auch möglich, dass Sie von Fehler # 29827 betroffen sind ; Versuchen Sie vielleicht, Ihre Version der Unit-Test-Suite von WP zu aktualisieren.
JD
@Sven: Ja, die Tests schlagen fehl. Ich habe installiert bin/install-wp-tests.sh(seit ich wp-cli benutzt habe) @JD: RegisterCustomPostType :: __ Konstrukt wird aufgerufen (habe gerade eine die()Anweisung hinzugefügt und phpunit bleibt dort stehen)
Ionut Staicu
Ich bin mir auf der Seite der Einheitentests nicht sicher (nicht meine Stärke), aber aus buchstäblicher Sicht können Sie did_action()überprüfen, ob Aktionen ausgelöst wurden.
Rarst
@Rarst: danke für den vorschlag, aber es funktioniert immer noch nicht. Aus irgendeinem Grund denke ich, dass das Timing falsch ist (Tests werden vor dem initHook ausgeführt).
Ionut Staicu

Antworten:

72

Einzeltest

Wenn Sie ein Plugin entwickeln, testen Sie es am besten, ohne die WordPress-Umgebung zu laden.

Wenn Sie Code schreiben, der ohne WordPress einfach getestet werden kann, wird Ihr Code besser .

Jede Komponente, die Unit-getestet wird, sollte isoliert getestet werden : Wenn Sie eine Klasse testen, müssen Sie nur diese bestimmte Klasse testen, vorausgesetzt, der gesamte andere Code funktioniert einwandfrei.

Der Isolator

Aus diesem Grund werden Unit-Tests als "Unit" bezeichnet.

Als zusätzlicher Vorteil wird Ihr Test ohne Laden des Kerns viel schneller ausgeführt.

Vermeiden Sie Haken im Konstruktor

Ein Tipp, den ich Ihnen geben kann, ist, dass Sie keine Haken in Konstruktoren stecken. Dies ist eines der Dinge, die Ihren Code für sich allein testbar machen.

Sehen wir uns den Testcode in OP an:

class CustomPostTypes extends WP_UnitTestCase {
  function test_custom_post_type_creation() {
    $this->assertTrue( post_type_exists( 'foo' ) );
  }
}

Nehmen wir an, dieser Test schlägt fehl . Wer ist der Täter ?

  • Wurde der Haken überhaupt nicht oder nicht richtig hinzugefügt?
  • Die Methode, die den Beitragstyp registriert, wurde überhaupt nicht oder mit falschen Argumenten aufgerufen.
  • gibt es einen Fehler in WordPress?

Wie kann es verbessert werden?

Angenommen, Ihr Klassencode lautet:

class RegisterCustomPostType {

  function init() {
    add_action( 'init', array( $this, 'register_post_type' ) );
  }

  public function register_post_type() {
    register_post_type( 'foo' );
  }
}

(Hinweis: Ich werde für den Rest der Antwort auf diese Version der Klasse verweisen.)

Die Art und Weise, wie ich diese Klasse geschrieben habe, ermöglicht es Ihnen, Instanzen der Klasse zu erstellen, ohne sie aufzurufen add_action.

In der obigen Klasse gibt es zwei Dinge zu testen:

  • Die Methode ruft init tatsächlich die add_actionÜbergabe der richtigen Argumente auf
  • Die Methode ruft register_post_type tatsächlich die register_post_typeFunktion auf

Ich habe nicht gesagt , dass Sie , wenn Post - Typen überprüfen vorhanden ist : wenn Sie die richtige Aktion hinzufügen und wenn Sie anrufen register_post_type, der benutzerdefinierte Post - Typ muss vorhanden sein: wenn es nicht existiert es ein Wordpress - Problem.

Denken Sie daran: Wenn Sie Ihr Plugin testen, müssen Sie Ihren Code testen , nicht WordPress-Code. In Ihren Tests müssen Sie davon ausgehen, dass WordPress (genau wie jede andere externe Bibliothek, die Sie verwenden) gut funktioniert. Das ist die Bedeutung von Unit Test.

Aber ... in der Praxis?

Wenn WordPress nicht geladen ist und Sie versuchen, die oben genannten Klassenmethoden aufzurufen, wird ein schwerwiegender Fehler angezeigt, sodass Sie die Funktionen verspotten müssen.

Die "manuelle" Methode

Sicher können Sie Ihre Spottbibliothek schreiben oder jede Methode "manuell" verspotten. Es ist möglich. Ich erkläre Ihnen, wie das geht, aber dann zeige ich Ihnen eine einfachere Methode.

Wenn WordPress nicht geladen wird, während Tests ausgeführt werden, können Sie seine Funktionen neu definieren, z . B. add_actionoder register_post_type.

Nehmen wir an, Sie haben eine Datei aus Ihrer Bootstrap-Datei geladen, in der Folgendes enthalten ist:

function add_action() {
  global $counter;
  if ( ! isset($counter['add_action']) ) {
    $counter['add_action'] = array();
  }
  $counter['add_action'][] = func_get_args();
}

function register_post_type() {
  global $counter;
  if ( ! isset($counter['register_post_type']) ) {
    $counter['register_post_type'] = array();
  }
  $counter['register_post_type'][] = func_get_args();
}

Ich habe die Funktionen umgeschrieben, um einfach jedes Mal, wenn sie aufgerufen werden, ein Element zu einem globalen Array hinzuzufügen.

Jetzt sollten Sie (falls Sie noch keine haben) eine eigene Basis-Testfallklassenerweiterung erstellen PHPUnit_Framework_TestCase, mit der Sie Ihre Tests einfach konfigurieren können.

Es kann so etwas sein wie:

class Custom_TestCase extends \PHPUnit_Framework_TestCase {

    public function setUp() {
        $GLOBALS['counter'] = array();
    }

}

Auf diese Weise wird vor jedem Test der globale Zähler zurückgesetzt.

Und jetzt dein Testcode (ich beziehe mich auf die neu geschriebene Klasse, die ich oben gepostet habe):

class CustomPostTypes extends Custom_TestCase {

  function test_init() {
     global $counter;
     $r = new RegisterCustomPostType;
     $r->init();
     $this->assertSame(
       $counter['add_action'][0],
       array( 'init', array( $r, 'register_post_type' ) )
     );
  }

  function test_register_post_type() {
     global $counter;
     $r = new RegisterCustomPostType;
     $r->register_post_type();
     $this->assertSame( $counter['register_post_type'][0], array( 'foo' ) );
  }

}

Sie sollten beachten:

  • Ich konnte die beiden Methoden separat aufrufen und WordPress ist überhaupt nicht geladen. Auf diese Weise weiß ich genau, wer der Schuldige ist , wenn ein Test fehlschlägt .
  • Wie gesagt, hier teste ich, dass die Klassen WP-Funktionen mit erwarteten Argumenten aufrufen. Es muss nicht geprüft werden, ob CPT wirklich vorhanden ist. Wenn Sie die Existenz von CPT testen, testen Sie das WordPress-Verhalten, nicht Ihr Plugin-Verhalten ...

Schön .. aber es ist ein PITA!

Ja, wenn Sie alle WordPress-Funktionen manuell verspotten müssen, ist das wirklich ein Schmerz. Ein allgemeiner Rat, den ich geben kann, ist, so wenig WP-Funktionen wie möglich zu verwenden: Sie müssen WordPress nicht umschreiben , sondern abstrakte WP-Funktionen, die Sie in benutzerdefinierten Klassen verwenden, damit sie verspottet und einfach getestet werden können.

Zum Beispiel können Sie für das obige Beispiel eine Klasse schreiben, die Post-Typen registriert register_post_typeund mit den angegebenen Argumenten 'init' aufruft . Mit dieser Abstraktion müssen Sie diese Klasse noch testen, aber an anderen Stellen Ihres Codes, an denen Beitragstypen registriert sind, können Sie diese Klasse verwenden und sie in Tests verspotten (vorausgesetzt, sie funktioniert).

Das Tolle ist, wenn Sie eine Klasse schreiben, die die CPT-Registrierung abstrahiert, können Sie ein separates Repository dafür erstellen und dank moderner Tools wie Composer in alle Projekte einbetten, in denen Sie es benötigen: einmal testen, überall verwenden . Und wenn Sie jemals einen Fehler darin finden, können Sie ihn an einer Stelle beheben, und mit einer einfachen Methode werden composer updatealle Projekte, in denen er verwendet wird, ebenfalls behoben.

Zum zweiten Mal: ​​Code zu schreiben, der isoliert getestet werden kann, bedeutet, besseren Code zu schreiben.

Aber früher oder später muss ich irgendwo WP-Funktionen verwenden ...

Na sicher. Du solltest niemals parallel zum Kern agieren , es macht keinen Sinn. Sie können Klassen schreiben, die WP-Funktionen umschließen, aber diese Klassen müssen ebenfalls getestet werden. Die oben beschriebene "manuelle" Methode kann für sehr einfache Aufgaben verwendet werden, aber wenn eine Klasse viele WP-Funktionen enthält, kann dies schmerzhaft sein.

Zum Glück gibt es dort gute Leute, die gute Dinge schreiben. 10up , eine der größten WP-Agenturen, unterhält eine großartige Bibliothek für Leute, die Plugins richtig testen möchten. Es ist WP_Mock.

Hiermit können Sie WP-Funktionen und Hooks nachahmen . Angenommen, Sie haben in Ihren Tests (siehe Repo-Readme) denselben Test geladen, den ich oben geschrieben habe:

class CustomPostTypes extends Custom_TestCase {

  function test_init() {
     $r = new RegisterCustomPostType;
     // tests that the action was added with given arguments
     \WP_Mock::expectActionAdded( 'init', array( $r, 'register_post_type' ) );
     $r->init();
  }

  function test_register_post_type() {
     // tests that the function was called with given arguments and run once
     \WP_Mock::wpFunction( 'register_post_type', array(
        'times' => 1,
        'args' => array( 'foo' ),
     ) );
     $r = new RegisterCustomPostType;
     $r->register_post_type();
  }

}

Einfach, nicht wahr? Diese Antwort ist kein Tutorial für WP_Mock, lesen Sie also die Repo-Readme für weitere Informationen, aber das obige Beispiel sollte ziemlich klar sein, denke ich.

Darüber hinaus müssen Sie keine verspotteten add_actionoder register_post_typeselbst erstellten oder globalen Variablen verwalten.

Und WP-Klassen?

WP hat auch einige Klassen, und wenn WordPress beim Ausführen von Tests nicht geladen ist, müssen Sie sie verspotten.

Das ist viel einfacher als das Verspotten von Funktionen. PHPUnit verfügt über ein eingebettetes System zum Verspotten von Objekten. Hier möchte ich Ihnen jedoch Mockery vorschlagen . Es ist eine sehr leistungsfähige Bibliothek und sehr einfach zu bedienen. Außerdem ist es eine Abhängigkeit von WP_Mock. Wenn Sie es haben, haben Sie auch Spott.

Aber was ist mit WP_UnitTestCase?

Die WordPress-Testsuite wurde erstellt, um den WordPress- Kern zu testen. Wenn Sie einen Beitrag zum Kern leisten möchten, ist dies von entscheidender Bedeutung. Wenn Sie sie jedoch nur für Plugins verwenden, werden Sie nicht isoliert getestet.

Werfen Sie einen Blick auf die Welt von WP: Es gibt viele moderne PHP-Frameworks und CMS. Keines davon empfiehlt, Plugins / Module / Erweiterungen (oder wie auch immer sie genannt werden) mit Framework-Code zu testen.

Wenn Sie Fabriken verpassen, ein nützliches Feature der Suite, müssen Sie wissen, dass es dort tolle Dinge gibt.

Fallstricke und Schattenseiten

Es gibt einen Fall, in dem der hier vorgeschlagene Workflow nicht funktioniert: Testen von benutzerdefinierten Datenbanken .

In der Tat, es zu schreiben , wenn Sie Standard - Wordpress - Tabellen und Funktionen (auf der untersten Ebene $wpdbMethoden) Sie nie brauchen tatsächlich Schreibdaten oder Test , wenn Daten tatsächlich in der Datenbank, nur sicher sein , dass richtigen Methoden mit der richtigen Argumenten aufgerufen werden.

Sie können jedoch Plug-ins mit benutzerdefinierten Tabellen und Funktionen schreiben, die Abfragen erstellen, um sie dort zu schreiben, und prüfen, ob diese Abfragen funktionieren, was Sie tun müssen.

In diesen Fällen kann Ihnen die WordPress-Testsuite sehr helfen, und das Laden von WordPress kann in einigen Fällen erforderlich sein, um Funktionen wie auszuführen dbDelta.

(Es ist nicht nötig zu sagen, dass für Tests eine andere Datenbank verwendet werden muss, oder?)

Glücklicherweise können Sie mit PHPUnit Ihre Tests in "Suites" organisieren, die separat ausgeführt werden können. So können Sie eine Suite für benutzerdefinierte Datenbanktests schreiben, in der Sie die WordPress-Umgebung (oder einen Teil davon) laden und den Rest Ihrer Tests WordPress-frei lassen .

Stellen Sie nur sicher, dass Sie Klassen schreiben, die so viele Datenbankoperationen wie möglich abstrahieren, so dass alle anderen Plug-in-Klassen von ihnen Gebrauch machen, damit Sie mithilfe von Mocks die Mehrheit der Klassen ordnungsgemäß testen können, ohne sich mit der Datenbank zu befassen.

Zum dritten Mal bedeutet das Schreiben von Code, der isoliert leicht zu testen ist, das Schreiben von besserem Code.

gmazzap
quelle
5
Heiliger Mist, viele nützliche Infos! Vielen Dank! Irgendwie habe ich es geschafft, den ganzen Punkt des Unit-Tests zu verpassen (bis jetzt habe ich PHP-Tests nur innerhalb von Code Dojo geübt). Ich habe heute auch früher von wp_mock erfahren, aber aus irgendeinem Grund kann ich es ignorieren. Was mich störte, war, dass jeder Test, egal wie klein er war, mindestens zwei Sekunden dauerte (zuerst WP env laden, dann den Test ausführen). Nochmals vielen Dank für das Öffnen meiner Augen!
Ionut Staicu
4
Danke @IonutStaicu Ich habe vergessen zu erwähnen, dass das Nichtladen von WordPress Ihre Tests viel schneller macht
gmazzap
6
Erwähnenswert ist auch, dass das WP Core-Unit-Test-Framework ein hervorragendes Tool für die Ausführung von INTEGRATION-Tests ist, bei denen es sich um automatisierte Tests handelt, um sicherzustellen, dass es sich gut in WP selbst integriert (z. B. gibt es keine zufälligen Kollisionen von Funktionsnamen usw.).
John P Bloch
1
@ JohnPBloch +1 für einen guten Punkt. Auch wenn die Verwendung eines Namespaces ausreicht, um Kollisionen von Funktionsnamen in WordPress zu vermeiden, wo alles global ist :) Aber Integrationen / Funktionstests sind eine Sache. Ich spiele im Moment mit Behat + Mink, aber ich übe immer noch damit.
gmazzap
1
Danke für den "Hubschrauberflug" über den UnitTest-Wald von WordPress - ich lache immer noch über dieses epische Bild ;-)
birgire