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:
- Die Klasse ist eine Sprachklasse, z
ArrayObject
- Die Klasse ist ein richtiges "Wertobjekt" .
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:
- es ist unveränderlich
- es gibt keine mögliche polymorphe Alternative
- Per Definition ist eine Wertobjektinstanz nicht von einer anderen zu unterscheiden, die dieselben Konstruktorargumente hat
Dies bedeutet, dass ein Wertobjekt als eigenständiger Typ angesehen werden kann, genau wie beispielsweise eine Zeichenfolge.
Wenn ein Code dies tut, ist $myEmail = 'some.email@example.com'
niemand besorgt darüber, diese Zeichenfolge zu verspotten, und auf die gleiche Weise sollte niemand besorgt darüber sein, eine Zeile wie zu verspotten new Email('some_name@example.com')
(vorausgesetzt, dies Email
ist unveränderlich und möglicherweise final
).
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:
add_action( 'activate_plugin', function( $plugin, $network_wide ) {
$message = My_Admin_Notices_Message( 'plugin', 'activated' );
if ( $message->has_text() ) {
$notice = new My_Notice( $plugin, $message, 'wpml-st-string-scan' );
$notice->add_action( new My_Admin_Notice_Action( 'Scan now', '#' ) );
$notice->add_action( new My_Admin_Notice_Action( 'Skip', '#', true ) );
$notices = new My_Notices();
$notices->add_notice( $notice );
$handler = new My_Admin_Notices_Handler( $notices );
$handler->handle_notices();
}
}, 10, 2);
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:
add_action( 'activate_plugin', function( $plugin, $network_wide ) {
$notice = $notice_factory->create_for( 'plugin', 'activated' );
if ( $notice instanceof My_Notice_Interface ) {
$handler_factory = new My_Admin_Notices_Handler_Factory();
$handler = $handler_factory->build_for_notices( [ $notice ] );
$handler->handle_notices();
}
}, 10, 2);
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:
class Foo {
public function run_something() {
$something = new Something();
$something->run();
}
}
Wenn Sie Tests ausführen können, ohne die Something
Klasse zu laden , können Sie eine benutzerdefinierte Something
Klasse nur zum Testen schreiben (einen "Stub").
Es ist immer besser, Stubs sehr einfach zu halten, z.
class Something{
public function run() {
return 'I ran'.
}
}
Wenn Tests ausgeführt werden, können Sie die Datei laden, die diesen Stub für die Something
Klasse enthält (z. B. aus Tests setUp()
). Wenn die zu testende Klasse a instanziiert new 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.
class Foo {
public function __construct() {
$this->something = new Something();
}
public function run_something() {
return $this->something->run();
}
}
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:
public function test_run_something() {
$foo = new Foo();
$something_mock = Mockery::mock( 'Something' );
$something_mock
->shouldReceive('run')
->once()
->withNoArgs()
->andReturn('I ran.');
// changing proxy properties will change private properties of proxied object
$proxy = new Andrew\Proxy( $foo );
$proxy->something = $something_mock;
$this->assertSame( 'I ran.', $foo->run_something() );
}