Grundlegendes zu PhpSpec

Wenn Sie PhpSpec mit anderen Testframeworks vergleichen, werden Sie feststellen, dass es sich um ein sehr ausgereiftes und meinungsstarkes Tool handelt. Einer der Gründe dafür ist, dass PhpSpec kein Testframework ist, wie Sie es bereits kennen. 

Stattdessen handelt es sich um ein Entwurfstool, das das Verhalten von Software beschreibt. Ein Nebeneffekt der Beschreibung des Verhaltens von Software mit PhpSpec besteht darin, dass Sie Spezifikationen erhalten, die später auch als Tests dienen.

In diesem Artikel werfen wir einen Blick unter die Haube von PhpSpec und versuchen, tiefer zu verstehen, wie es funktioniert und wie man es benutzt.

Wenn Sie phpspec auffrischen möchten, werfen Sie einen Blick auf mein Einführungs-Tutorial.

In diesem Artikel…

  • Eine kurze Einführung in die Internen von PhpSpec
  • Der Unterschied zwischen TDD und BDD
  • Wie unterscheidet sich PhpSpec (von PHPUnit)
  • PhpSpec: Ein Design-Tool

Eine kurze Einführung in die Internen von PhpSpec

Sehen wir uns zunächst einige der wichtigsten Konzepte und Klassen an, die PhpSpec bilden.

Verstehen $ das

Verstehe was $ das bezieht sich auf den Schlüssel, um zu verstehen, wie sich PhpSpec von anderen Tools unterscheidet. Grundsätzlich gilt, $ das beziehen sich auf eine Instanz der tatsächlich getesteten Klasse. Versuchen wir, dies etwas näher zu untersuchen, um besser zu verstehen, was wir meinen.

Zunächst brauchen wir eine Spezifikation und eine Klasse, um damit herumzuspielen. Wie Sie wissen, machen die Generatoren von PhpSpec das für uns sehr einfach:

$ phpspec desc "Suhm \ HelloWorld" $ Phpspec Run Soll ich "Suhm \ HelloWorld" für Sie erstellen? y 

Als nächstes öffnen Sie die generierte Spezifikationsdatei und lassen Sie uns versuchen, ein wenig mehr Informationen darüber zu erhalten $ das:

shouldHaveType ('Suhm \ HelloWorld'); var_dump (get_class ($ this));  

get_class () gibt den Klassennamen eines bestimmten Objekts zurück. In diesem Fall werfen wir einfach $ das dort zu sehen, was es zurückgibt:

$ string (24) "spec \ Suhm \ HelloWorldSpec"

Okay, also nicht überraschend, get_class () sagt uns das $ das ist ein Beispiel von spec \ Suhm \ HelloWorldSpec. Das macht Sinn, denn das ist doch so gerade einfacher alter PHP-Code. Wenn wir stattdessen verwendet haben get_parent_class (), wir würden bekommenPhpSpec \ ObjectBehavior, da unsere Spezifikation diese Klasse erweitert.

Denken Sie daran, ich habe es Ihnen gerade gesagt $ das tatsächlich auf die zu prüfende Klasse bezogen, was wäreSuhm \ HelloWorld in unserem Fall? Wie Sie sehen, ist der Rückgabewert von get_class ($ this) widerspricht mit $ this-> sollteHaveType ('Suhm \ HelloWorld');.

Lass uns etwas anderes ausprobieren:

shouldHaveType ('Suhm \ HelloWorld'); var_dump (get_class ($ this)); $ this-> dumpThis () -> shouldReturn ('spec \ Suhm \ HelloWorldSpec');  

Mit dem obigen Code versuchen wir, eine Methode mit dem Namen aufzurufen dumpThis () auf der Hallo Welt Beispiel. Wir verketten eine Erwartung an den Methodenaufruf und erwarten, dass der Rückgabewert der Funktion eine Zeichenfolge ist, die enthält"spec \ Suhm \ HelloWorldSpec". Dies ist der Rückgabewert von get_class () in der Zeile darüber.

Auch hier können uns die PhpSpec-Generatoren mit einigen Gerüsten helfen:

$ phpspec run Soll ich 'Suhm \ HelloWorld :: dumpThis ()' für Sie erstellen? y 

Versuchen wir mal anzurufen get_class () von innen dumpThis () auch:

Wiederum nicht überraschend erhalten wir:

 10, es ist initialisierbar "spec \ Suhm \ HelloWorldSpec", aber "Suhm \ HelloWorld". 

Es sieht so aus, als würden wir hier etwas vermissen. Ich habe damit angefangen, Ihnen das zu sagen $ das bezieht sich nicht auf das, was Sie Ihrer Meinung nach tun, aber bisher haben unsere Experimente nichts Unerwartetes gezeigt. Außer einer Sache: Wie können wir anrufen? $ this-> dumpThis () vorher gab es ohne PHP Quietschen bei uns?

Um dies zu verstehen, müssen wir in den Quellcode von PhpSpec eintauchen. Wenn Sie selbst einen Blick darauf werfen möchten, können Sie den Code auf GitHub lesen.

Schauen Sie sich den folgenden Code an src / PhpSpec / ObjectBehavior.php (die Klasse, die unsere Spezifikation erweitert):

/ ** * Proxies rufen alle den PhpSpec-Betreff auf * * @param string $ method * @param array $ arguments * * @return mixed * / public function __call ($ method, array $ arguments = array ()) return call_user_func_array ( Array ($ this-> object, $ method), $ arguments);  

Die Kommentare geben das meiste davon weg: "Proxies rufen alle zum Thema PhpSpec auf". Das PHP __Anruf method ist eine magische Methode, die automatisch aufgerufen wird, wenn eine Methode nicht verfügbar ist (oder nicht vorhanden ist).. 

Dies bedeutet, dass wir versucht haben anzurufen $ this-> dumpThis (), Der Anruf wurde anscheinend dem PhpSpec-Thema unterstellt. Wenn Sie sich den Code ansehen, können Sie sehen, dass der Methodenaufruf an ihn weitergeleitet wird $ this-> object. (Dasselbe gilt für Eigenschaften in unserem Beispiel. Sie werden alle ebenfalls mit anderen magischen Methoden dem Subjekt unterstellt. Schauen Sie in die Quelle, um sich selbst zu überzeugen.)

Lassen Sie uns konsultieren get_class () noch einmal und sehen, worüber es zu sagen hat $ this-> object:

shouldHaveType ('Suhm \ HelloWorld'); var_dump (get_class ($ this-> object));  

Und schau was wir bekommen:

Zeichenfolge (23) "PhpSpec \ Wrapper \ Subject"

Mehr dazu Gegenstand

Gegenstand ist ein Wrapper und implementiert das PhpSpec \ Wrapper \ WrapperInterface. Es ist ein Kernbestandteil von PhpSpec und lässt all die scheinbare Magie des Frameworks zu. Es umschließt eine Instanz der Klasse, die wir testen, sodass wir alle möglichen Arten von Dingen ausführen können, z. B. das Aufrufen von Methoden und Eigenschaften, die nicht vorhanden sind und die Erwartungen festlegen. 

Wie bereits erwähnt, ist PhpSpec sehr zuversichtlich, wie Sie Ihren Code schreiben und spezifizieren sollten. Eine Spezifikation ist einer Klasse zugeordnet. Du hast nur ein Betreff pro Spezifikation, den PhpSpec sorgfältig für Sie einwickelt. Das Wichtigste dabei ist, dass Sie dies verwenden können $ das als ob es die eigentliche Instanz war und für wirklich lesbare und aussagekräftige Angaben sorgt.

PhpSpec enthält a Verpackung das sorgt für die Instantiierung der Gegenstand. Es packt die Gegenstand mit dem eigentlichen Objekt spezifizieren wir. Schon seit Gegenstand implementiert die WrapperInterface es muss eine haben getWrappedObject ()Methode, die uns den Zugriff auf das Objekt ermöglicht. Dies ist die Objektinstanz, nach der wir zuvor gesucht haben get_class ()

Lass es uns noch einmal versuchen:

shouldHaveType ('Suhm \ HelloWorld'); var_dump (get_class ($ this-> object-> getWrappedObject ())); // Und nur um ganz sicher zu sein: var_dump ($ this-> object-> getWrappedObject () -> dumpThis ());  

Und los gehts:

$ vendor / bin / phpspec run string (15) Zeichenfolge "Suhm \ HelloWorld" (15) "Suhm \ HelloWorld" 

Auch wenn sich hinter den Kulissen vieles abspielt, arbeiten wir am Ende immer noch mit der eigentlichen Objektinstanz von Suhm \ HelloWorld. Alles ist gut.

Früher, als wir angerufen haben $ this-> dumpThis (), Wir haben erfahren, wie der Anruf tatsächlich an die Telefonzentrale gerichtet wurde Gegenstand. Das haben wir auch gelernt Gegenstand ist nur ein Wrapper und nicht das eigentliche Objekt. 

Mit diesem Wissen ist klar, dass wir nicht anrufen können dumpThis () auf Gegenstand ohne eine andere magische Methode. Gegenstand hat ein __Anruf() Methode auch:

/ ** * @param Zeichenfolge $ -Methode * @param Array $ Argumente * * @return gemischt | Betreff * / public Funktion __call ($ -Methode, Array $ Argumente = Array ()) if (0 === strpos ($ -Methode) , 'sollte')) return $ this-> callExpectation ($ method, $ arguments);  $ this-> caller-> call zurückgeben ($ -Methode, $ Argumente);  

Diese Methode hat zwei Möglichkeiten. Zuerst wird geprüft, ob der Methodenname mit 'soll' beginnt. Wenn dies der Fall ist, ist dies eine Erwartung, und der Aufruf wird an eine aufgerufene Methode delegiert callExpectation (). Wenn nicht, wird der Anruf stattdessen an eine Instanz von delegiert PhpSpec \ Wrapper \ Subject \ Caller

Wir werden das ignorieren Anrufer zur Zeit. Es enthält auch das umschlossene Objekt und weiß, wie man Methoden darauf aufruft. Das Anrufer gibt eine umschlossene Instanz zurück, wenn Methoden zu diesem Thema aufgerufen werden, wodurch die Erwartungen an Methoden gekettet werden können, wie dies bei uns der Fall war dumpThis ().

Schauen wir uns stattdessen das an callExpectation () Methode:

/ ** * @param string $ method * @param array $ arguments * * @return mixed * / private Funktion callExpectation ($ method, array $ arguments) $ subject = $ this-> makeSureWeHaveASubject (); $ expectation = $ this-> expectationFactory-> create ($ method, $ subject, $ arguments); if (0 === strpos ($ method, 'shouldNot')) return $ erwartet -> Übereinstimmung (lcfirst (substr ($ method, 9)), $ this, $ arguments, $ this-> wrappedObject);  return $ expectation-> match (lcfirst (substr ($ method, 6)), $ this, $ arguments, $ this-> wrappedObject);  

Diese Methode ist für das Erstellen einer Instanz von verantwortlich PhpSpec \ Wrapper \ Subject \ Expectation \ ExpectationInterface. Diese Schnittstelle schreibt ein Spiel() Methode, die die callExpectation () Anrufe zur Überprüfung der Erwartung. Es gibt vier verschiedene Arten von Erwartungen: PositivNegativPositiveThrow und NegativeThrow. Jede dieser Erwartungen enthält eine Instanz von PhpSpec \ Matcher \ MatcherInterface dass die Spiel() Methode verwendet. Lassen Sie uns als nächstes Matcher betrachten.

Matchers

Matcher verwenden wir, um das Verhalten unserer Objekte zu bestimmen. Wann immer wir schreiben sollte…  oder sollte nicht… , Wir benutzen einen Matcher. Eine umfassende Liste der PhpSpec-Matcher finden Sie in meinem persönlichen Blog.

PhpSpec enthält viele Matcher, die alle erweitern PhpSpec \ Matcher \ BasicMatcher Klasse, die das implementiert MatcherInterface. Die Art und Weise, wie Matchers arbeiten, ist ziemlich einfach. Lassen Sie uns es zusammen betrachten und ich möchte Sie dazu ermutigen, sich auch den Quellcode anzusehen.

Betrachten wir als Beispiel diesen Code aus der IdentityMatcher:

/ ** * @ var array * / private static $ keywords = array ('return', 'be', 'gleich', 'beEqualTo'); / ** * @param string $ name * @param gemischt $ subject * @param array $ argumente * * @return bool * / public Funktion unterstützt ($ name, $ subject, Array $ arguments) return in_array ($ name, self.) :: $ keywords) && 1 == count ($ arguments);  

Das unterstützt () Methode wird durch die diktiert MatcherInterface. In diesem Fall vier Aliase sind für den Matcher in definiert $ Schlüsselwörter Array. Dadurch kann der Matcher entweder Folgendes unterstützen: shouldReturn ()sollte sein()shouldEqual () odershouldBeEqualTo (), oder shouldNotReturn ()sollte nicht()shouldNotEqual () oder shouldNotBeEqualTo ().

Von dem BasicMatcher, Zwei Methoden werden vererbt: positiveMatch () und negativeMatch (). Sie sehen so aus:

/ ** * @param Zeichenfolge $ name * @param mixed $ subject * @param array $ argumente * * @return mixed * * @throws FailureException * / final öffentliche Funktion positiveMatch ($ name, $ subject, Array $ arguments) if (false === $ this-> match ($ subject, $ arguments)) werfen $ this-> getFailureException ($ name, $ subject, $ arguments);  return $ subject;  

Das positiveMatch () Methode löst eine Ausnahme aus, wenn die Streichhölzer() Methode (abstrakte Methode, die Matcher implementieren müssen) gibt zurück falsch. Das negativeMatch () Methode funktioniert umgekehrt. Das Streichhölzer() Methode für dieIdentityMatcher verwendet die === Operator, um das zu vergleichen $ subject mit dem Argument für die Matcher-Methode:

/ ** * @param mixed $ subject * @param array $ arguments * * @return bool * / protected () entspricht der Funktion ($ subject, Array $ arguments) return $ subject === $ arguments [0];  

Wir könnten den Matcher so verwenden:

$ this-> getUser () -> shouldNotBeEqualTo ($ anotherUser); 

Welches würde schließlich anrufen negativeMatch () und stellen Sie sicher, dass Streichhölzer() gibt falsch zurück.

Schauen Sie sich einige der anderen Streichhölzer an und sehen Sie, was sie tun!

Versprechen von mehr Magie

Bevor wir diese kurze Tour durch die Internen von PhpSpec beenden, werfen wir einen Blick auf ein weiteres Stück Magie:

shouldHaveType ('Suhm \ HelloWorld'); var_dump (get_class ($ object));  

Durch Hinzufügen des angedeuteten Typs $ object Parameter In unserem Beispiel wird PhpSpec Reflection automatisch verwenden, um eine Instanz der Klasse für uns zu injizieren. Aber bei den Dingen, die wir bereits gesehen haben, vertrauen wir wirklich darauf, dass wir wirklich eine Instanz bekommen StdClass? Lassen Sie uns konsultieren get_class () ein Mal noch:

$ vendor / bin / phpspec-Laufzeichenfolge (28) "PhpSpec \ Wrapper \ Collaborator" 

Nee. Anstatt StdClass Wir bekommen eine Instanz von PhpSpec \ Wrapper \ Collaborator. Um was geht es hierbei?

Mögen GegenstandMitarbeiter ist ein Wrapper und implementiert das WrapperInterface. Es packt eine Instanz von\ Prophezeiung \ Prophezeiung \ ObjectProphecy, Das stammt von Prophecy, dem spöttischen Rahmen, der mit PhpSpec zusammenarbeitet. Anstelle eines StdClass So gibt uns PhpSpec einen Spott. Das macht Spott mit PhpSpec lächerlich und wir können unseren Objekten Versprechungen wie diese hinzufügen:

$ user-> getAge () -> willReturn (10); $ this-> setUser ($ user); $ this-> getUserStatus () -> shouldReturn ('child'); 

Bei dieser kurzen Tour durch Teile von PhpSpecs Interna hoffe ich, dass Sie mehr als nur ein einfaches Testframework sind.

Der Unterschied zwischen TDD und BDD

PhpSpec ist ein Werkzeug für SpecBDD. Um ein besseres Verständnis zu erlangen, betrachten wir die Unterschiede zwischen testgetriebener Entwicklung (TDD) und verhaltensgesteuerter Entwicklung (BDD). Anschließend werden wir uns kurz ansehen, wie sich PhpSpec von anderen Tools wie PHPUnit unterscheidet.

TDD ist das Konzept, automatisierte Tests die Gestaltung und Implementierung von Code vorantreiben zu lassen. Wenn wir kleine Tests für jedes Feature schreiben, bevor wir sie tatsächlich implementieren, und wenn wir einen Bestanden-Test erhalten, wissen wir, dass unser Code diese spezifische Funktion erfüllt. Nach einem Refactoring beenden wir die Codierung und schreiben stattdessen den nächsten Test. Das Mantra ist "Rot", "Grün", "Refaktor"!

BDD hat seinen Ursprung in - und ist TDD sehr ähnlich. Ehrlich gesagt handelt es sich hauptsächlich um eine Formulierung, die in der Tat wichtig ist, da sie unsere Denkweise als Entwickler verändern kann. Während TDD über Tests spricht, spricht BDD über die Beschreibung von Verhalten. 

Mit TDD konzentrieren wir uns darauf, zu überprüfen, ob unser Code so funktioniert, wie wir es erwarten, während wir uns bei BDD darauf konzentrieren, zu überprüfen, ob sich unser Code tatsächlich so verhält, wie wir es wünschen. Ein Hauptgrund für die Entstehung von BDD als Alternative zu TDD ist die Vermeidung des Wortes "Test". Mit BDD sind wir nicht wirklich daran interessiert, die Implementierung unseres Codes zu testen. Wir sind mehr daran interessiert, was er macht (sein Verhalten). Wenn wir BDD anstelle von TDD machen, haben wir Geschichten und Spezifikationen. Dies macht das Schreiben traditioneller Tests überflüssig.

Geschichten und Spezifikationen sind eng mit den Erwartungen der Projektbeteiligten verbunden. Das Schreiben von Geschichten (mit einem Tool wie Behat) würde vorzugsweise zusammen mit den Stakeholdern oder Domänenexperten erfolgen. Die Geschichten behandeln das äußere Verhalten. Wir verwenden Spezifikationen, um das interne Verhalten zu entwerfen, das zum Erfüllen der Schritte der Geschichten erforderlich ist. Jeder Schritt in einer Story erfordert möglicherweise mehrere Iterationen mit Schreibvorgaben und Implementieren von Code, bevor er zufrieden ist. Unsere Geschichten zusammen mit unseren Spezifikationen helfen uns sicherzustellen, dass wir nicht nur ein funktionierendes Ding bauen, sondern dass es auch das Richtige ist. BDD hat also viel mit Kommunikation zu tun.

Wie unterscheidet sich PhpSpec von PHPUnit??

Vor einigen Monaten hat ein bemerkenswertes Mitglied der PHP-Community, Mathias Verraes, auf Twitter "Ein Unit-Testing-Framework in einem Tweet" gepostet. Es ging darum, den Quellcode eines Frameworks zum Testen funktionaler Einheiten in einen einzigen Tweet zu integrieren. Wie Sie der Übersicht entnehmen können, ist der Code wirklich funktionsfähig und ermöglicht Ihnen das Schreiben grundlegender Komponententests. Das Konzept des Komponententests ist eigentlich ziemlich einfach: Prüfen Sie eine Art von Behauptung und informieren Sie den Benutzer über das Ergebnis.

Natürlich sind die meisten Testframeworks wie PHPUnit in der Tat viel fortgeschrittener und können weitaus mehr als das Mathias-Framework, aber es zeigt immer noch einen wichtigen Punkt: Sie machen etwas geltend und dann führt das Framework diese Assertion für Sie durch.

Schauen wir uns einen sehr grundlegenden PHPUnit-Test an:

public function testTrue () $ this-> assertTrue (false);  

Könnten Sie eine sehr einfache Implementierung eines Testframeworks schreiben, das diesen Test ausführen könnte? Ich bin mir ziemlich sicher, dass die Antwort "Ja" ist, dass Sie das tun könnten. Immerhin das einzige, was die assertTrue () Methode muss tun, um einen Wert mit zu vergleichen wahr und eine Ausnahme auslösen, wenn es fehlschlägt. Im Grunde ist das, was los ist, eigentlich ziemlich geradlinig.

Wie unterscheidet sich PhpSpec also? Zunächst einmal ist PhpSpec kein Testwerkzeug. Das Testen Ihres Codes ist nicht das Hauptziel von PhpSpec, aber es wird zu einem Nebeneffekt, wenn Sie die Software dazu verwenden, Ihre Software durch schrittweises Hinzufügen von Spezifikationen für das Verhalten zu entwickeln (BDD).. 

Zweitens, ich denke, die obigen Abschnitte sollten bereits deutlich gemacht haben, wie sich PhpSpec unterscheidet. Lass uns noch etwas Code vergleichen:

// PhpSpec-Funktion it_is_initializable () $ this-> shouldHaveType ('Suhm \ HelloWorld');  // PHPUnit-Funktion testIsInitializable () $ object = new Suhm \ HelloWorld (); $ this-> assertInstanceOf ('Suhm \ HelloWorld', $ object);  

Da PhpSpec hochmeinend ist und einige Aussagen darüber macht, wie unser Code gestaltet ist, können wir unseren Code sehr einfach beschreiben. Auf der anderen Seite macht PHPUnit keine Aussagen zu unserem Code und lässt uns so ziemlich tun, was wir wollen. Grundsätzlich alles, was PHPUnit für uns in diesem Beispiel macht, ist auszuführen $ object gegen dasBeispielvon Operator. 

Auch wenn PHPUnit einfacher zu sein scheint (ich glaube nicht, dass es so ist), können Sie, wenn Sie nicht aufpassen, leicht in Fallen von schlechtem Design und Architektur geraten, weil Sie damit fast alles tun können. Allerdings kann PHPUnit für viele Anwendungsfälle immer noch großartig sein, aber es ist kein Design-Tool wie PhpSpec. Es gibt keine Anleitung - Sie müssen wissen, was Sie tun.

PhpSpec: Ein Design-Tool

Auf der PhpSpec-Website können wir erfahren, dass PhpSpec:

Ein PHP-Toolset, um das aufkommende Design anhand der Spezifikationen zu steuern.

Lassen Sie es mich noch einmal sagen: PhpSpec ist kein Testframework. Es ist ein Entwicklungswerkzeug. Ein Software-Design-Tool. Es ist kein einfaches Assertions-Framework, das Werte vergleicht und Ausnahmen auslöst. Es ist ein Werkzeug, das uns beim Entwerfen und Erstellen gut entwickelten Codes unterstützt. Es erfordert, dass wir über die Struktur unseres Codes nachdenken und bestimmte Architekturmuster durchsetzen, bei denen eine Klasse einer Spezifikation entspricht. Wenn Sie gegen das Prinzip der Einzelverantwortung verstoßen und teilweise etwas verspotten müssen, dürfen Sie dies nicht tun.

Alles Gute zum Thema!

Oh! Und schließlich = Da PhpSpec selbst spezifiziert ist, schlage ich vor, dass Sie zu GitHub gehen und die Quelle erkunden, um mehr zu erfahren.