Dies kann als Folge des Rückrufs von Testing Hooks angesehen werden .
Das Problem: Ich möchte eine Klasse testen, die eine neue Instanz einer My_Notice
außerhalb des Plugins definierten Klasse erstellt (nennen wir sie das "Haupt-Plugin").
Mein Unit-Test weiß nichts darüber, My_Notice
da er in einer Bibliothek eines Drittanbieters definiert ist (genauer gesagt in einem anderen Plugin). Daher habe ich folgende Optionen (soweit ich weiß):
- Stub die
My_Notice
Klasse: schwer zu pflegen - Fügen Sie die benötigten Dateien aus der Bibliothek eines Drittanbieters hinzu: Dies funktioniert möglicherweise, aber ich mache meine Tests weniger isoliert
- Dynamisches Stubben der Klasse: Ich bin mir nicht sicher, ob dies überhaupt möglich ist, aber es wäre sehr ähnlich, eine Klasse zu verspotten, außer dass sie auch die Definition derselben erstellen sollte, damit die Klasse, die ich teste, sie instanziieren kann.
Die Antwort von @gmazzap weist darauf hin, dass wir diese Art von Abhängigkeiten vermeiden sollten, dem stimme ich voll und ganz zu. Und die Idee, Stub-Klassen zu erstellen, scheint mir nicht gut zu sein: Ich würde lieber den Code des "Main Plugins" mit allen Konsequenzen einfügen.
Ich sehe jedoch nicht, wie ich es anders machen kann.
Hier ist ein Beispiel für den Code, den ich testen möchte:
class My_Admin_Notices_Handler {
public function __construct( My_Notices $admin_notices ) {
$this->admin_notices = $admin_notices;
}
/**
* This will be hooked to the `activated_plugin` action
*
* @param string $plugin
* @param bool $network_wide
*/
public function activated_plugin( $plugin, $network_wide ) {
$this->add_notice( 'plugin', 'activated', $plugin );
}
/**
* @param string $type
* @param string $action
* @param string $plugin
*/
private function add_notice( $type, $action, $plugin ) {
$message = '';
if ( 'activated' === $action ) {
if ( 'plugin' === $type ) {
$message = __( '%1s Some message for plugin(s)', 'my-test-domain' );
}
if ( 'theme' === $type ) {
$message = __( '%1s Some message for the theme', 'my-test-domain' );
}
}
if ( 'updated' === $action && ( 'plugin' === $type || 'theme' === $type ) ) {
$message = __( '%1s Another message for updated theme or plugin(s)', 'my-test-domain' );
}
if ( $message ) {
$notice = new My_Notice( $plugin, 'wpml-st-string-scan' );
$notice->text = $message;
$notice->actions = array(
new My_Admin_Notice_Action( __( 'Scan now', 'my-test-domain' ), '#' ),
new My_Admin_Notice_Action( __( 'Skip', 'my-test-domain' ), '#', true ),
);
$this->admin_notices->add_notice( $notice );
}
}
}
Grundsätzlich hat diese Klasse eine Methode, an die gebunden wird activated_plugin
. Diese Methode erstellt eine Instanz einer "Benachrichtigungs" -Klasse, die irgendwo von der My_Notices
an den Konstruktor übergebenen Instanz gespeichert wird .
Der Konstruktor der My_Notice
Klasse erhält zwei grundlegende Argumente (eine UID und eine "Gruppe") und erhält einige Eigenschaften (beachten Sie, dass das gleiche Problem bei der My_Admin_Notice_Action
Klasse auftritt).
Wie könnte ich die My_Notice
Klasse zu einer injizierten Abhängigkeit machen?
Natürlich könnte ich ein assoziatives Array verwenden, eine Aktion aufrufen, die vom "Haupt-Plugin" verknüpft wird und dieses Array in die Argumente der Klasse übersetzt, aber für mich sieht es nicht sauber aus.
quelle
Antworten:
Ist eine Objektinjektion wirklich notwendig?
Um isoliert vollständig testbar zu sein, sollte Code vermeiden, Klassen direkt zu instanziieren und alle Objekte, die benötigt werden, in Objekte einzufügen.
Es gibt jedoch mindestens zwei Ausnahmen von dieser Regel:
ArrayObject
Keine Notwendigkeit, die Injektion für Kernobjekte zu erzwingen
Der erste Fall ist leicht zu erklären: Es ist etwas, das in die Sprache eingebettet ist, also nehmen Sie einfach an, dass es funktioniert. Andernfalls sollten Sie auch alle PHP-Kernfunktionen oder -Anweisungen wie
return
… testen.Keine Notwendigkeit, die Injektion für das Wertobjekt zu erzwingen
Der zweite Fall bezieht sich auf die Art eines Wertobjekts. Eigentlich:
Dies bedeutet, dass ein Wertobjekt als eigenständiger Typ angesehen werden kann, genau wie beispielsweise eine Zeichenfolge.
Wenn ein Code dies tut, ist
$myEmail = '[email protected]'
niemand besorgt darüber, diese Zeichenfolge zu verspotten, und auf die gleiche Weise sollte niemand besorgt darüber sein, eine Zeile wie zu verspottennew Email('[email protected]')
(vorausgesetzt, diesEmail
ist unveränderlich und möglicherweisefinal
).Denn was ich aus Ihrem Code erraten kann,
My_Admin_Notice_Action
ist ein guter Kandidat, um ein Wertobjekt zu sein / zu werden. Aber ich kann nicht sicher sein, ohne den Code zu sehen.Ich kann nicht dasselbe sagen
My_Notice
, aber das ist nur eine andere Vermutung.Wenn eine Injektion erforderlich ist…
Falls die Klasse, die in einer anderen Klasse instanziiert wird, nicht einer der beiden oben genannten Fälle ist, ist es sicherlich besser, sie zu injizieren.
Wie im Beispiel in OP benötigt die zu erstellende Klasse jedoch Argumente, die vom Kontext abhängen.
In diesem Fall gibt es keine "eine" Antwort, sondern unterschiedliche Ansätze, die je nach Fall alle gültig sein können.
Instanziierung im Client-Code
Ein einfacher Ansatz besteht darin, "Objektcode" von "Clientcode" zu trennen. Wobei Client-Code der Code ist, der Objekte verwendet.
Auf diese Weise können Sie Objektcode mithilfe von Komponententests testen und Client-Codetests Funktions- / Integrationstests überlassen, bei denen Sie sich nicht um Isolation kümmern müssen.
In Ihrem Fall wird es so etwas wie:
Ich habe einige Vermutungen zu Ihrem Code angestellt und Methoden und Klassen geschrieben, die möglicherweise nicht existieren (wie
My_Admin_Notices_Message
), aber der Punkt hier ist, dass der obige Abschluss den gesamten Client-Code enthält, der zum Instanziieren und "Verwenden" der Objekte erforderlich ist. Sie können Ihre Objekte dann isoliert testen, da keines dieser Objekte andere Objekte instanziieren muss, aber alle die erforderlichen Instanzen im Konstruktor oder als Methodenparameter erhalten.Vereinfachen Sie den Client-Code mit Fabriken
Der obige Ansatz funktioniert möglicherweise gut für kleine Plugins (oder einen kleinen Teil eines Plugins, der vom Rest des Plugins isoliert werden kann), aber für größere Codebasen, wenn Sie nur diesen Ansatz verwenden, können Sie unter anderem mit großem Client-Code in Closures enden Dinge, ist sehr schwer zu testen und zu warten.
In diesen Fällen können Ihnen Fabriken helfen. Fabriken sind Objekte mit dem alleinigen Umfang, andere Objekte zu kreten. Meistens ist es gut, bestimmte Fabriken für Objekte desselben Typs zu haben (Implementierung derselben Schnittstelle).
Bei Fabriken könnte der obige Code folgendermaßen aussehen:
Der gesamte Instanziierungscode befindet sich in Fabriken. Sie können Fabriken weiterhin isoliert testen, da Sie testen müssen, ob sie bei richtigen Argumenten erwartete Klassen erzeugen (oder bei falschen Argumenten erwartete Fehler erzeugen).
Und dennoch können Sie alle anderen Objekte isoliert testen, da keine Objekte Instanzen erstellen müssen. In Fakten befindet sich der Instanziierungscode ausschließlich in Fabriken.
Denken Sie natürlich daran, dass Wertobjekte keine Fabriken benötigen. Es wäre, als würden Sie Fabriken für Zeichenfolgen erstellen.
Was ist, wenn ich den Code nicht ändern kann?
Stubs
Manchmal ist es aus verschiedenen Gründen nicht möglich, den Code zu ändern, der andere Objekte instanziiert. ZB ist Code von Drittanbietern, Abwärtskompatibilität und so weiter.
In diesen Fällen können Sie einige Stubs schreiben, wenn es möglich ist, die Tests auszuführen, ohne die zu instanziierenden Klassen zu laden.
Nehmen wir an, Sie haben einen Code, der Folgendes tut:
Wenn Sie Tests ausführen können, ohne die
Something
Klasse zu laden , können Sie eine benutzerdefinierteSomething
Klasse nur zum Testen schreiben (einen "Stub").Es ist immer besser, Stubs sehr einfach zu halten, z.
Wenn Tests ausgeführt werden, können Sie die Datei laden, die diesen Stub für die
Something
Klasse enthält (z. B. aus TestssetUp()
). Wenn die zu testende Klasse a instanziiertnew Something
, testen Sie sie isoliert, da die Einrichtung sehr einfach ist und Sie sie erstellen können Ein Weg, der von Natur aus das tut, was Sie erwarten.Natürlich ist dies nicht sehr einfach zu warten, aber wenn man bedenkt, dass man normalerweise keinen Code von Drittanbietern testet, muss man dies selten tun.
Manchmal ist dies jedoch hilfreich, um isolierten Plugins / Themes-Code zu testen, der WordPress-Objekte instanziiert (z
WP_Post
. B. ).Spottüberladung
Mit
Mockery
(einer Bibliothek, die Tools für PHP-Unit-Tests bereitstellt) können Sie sogar vermeiden, diese Stubs zu schreiben. Mit Mockery ist "Instance Mock" (auch bekannt als "Overload") möglich, die Erstellung neuer Instanzen abzufangen und durch ein Mock zu ersetzen. Dieser Artikel erklärt ziemlich gut, wie es geht.Wenn die Klasse geladen ist ...
Wenn der zu testende Code starke Abhängigkeiten aufweist (Klassen mithilfe von instanziieren
new
) und es keine Möglichkeit gibt, Tests zu laden, ohne die zu instanziierende Klasse zu laden, besteht nur eine sehr geringe Wahrscheinlichkeit, ihn isoliert zu testen, ohne den Code zu berühren (oder eine Abstraktion zu schreiben) Schicht darum herum).Beachten Sie jedoch, dass die Test-Bootstrap-Datei häufig als erstes geladen wird. Es gibt also mindestens zwei Fälle, in denen Sie das Laden Ihrer Stubs erzwingen können:
Code verwendet einen Autoloader. Wenn Sie in diesem Fall die Stubs vor dem Laden des Autoloaders laden, wird die "echte" Klasse nie geladen, da bei
new
Verwendung die Klasse bereits gefunden und der Autoloader nicht ausgelöst wird.Code prüft
class_exists
vor dem Definieren / Laden der Klasse. In diesem Fall wird durch das Laden der Stubs als Erstes verhindert, dass die "echte" Klasse geladen wird.Der knifflige letzte Ausweg
Wenn alles andere fehlschlägt, können Sie noch etwas tun, um harte Abhängigkeiten zu testen.
Sehr oft werden harte Abhängigkeiten als private Variablen gespeichert.
In solchen Fällen können Sie die harten Abhängigkeiten nach dem Erstellen der Instanz durch einen Mock / Stub ersetzen .
Dies liegt daran, dass sogar
private
Eigenschaften in PHP (ziemlich) einfach ersetzt werden können. Eigentlich in mehr als einer Hinsicht.Ohne auf Details einzugehen, kann ich sagen, dass die Verschlussbindung dazu verwendet werden kann. Ich habe sogar eine Bibliothek namens "Andrew" geschrieben , die für den Bereich verwendet werden kann.
Mit Andrew (und Mockery) zum Testen der
Foo
obigen Klasse können Sie Folgendes tun:quelle