Spott: Ein besserer Weg
German (Deutsch) translation by Wei Zhang (you can also view the original English article)
Mockery ist eine PHP-Erweiterung, die vor allem im Vergleich zu PHPUnit ein hervorragendes Mock-Erlebnis bietet. Während das spöttische Framework von PHPUnit mächtig ist, bietet Mockery eine natürlichere Sprache mit einem Hamcrest-ähnlichen Satz von Matchern. In diesem Artikel vergleiche ich die beiden spöttischen Frameworks und hebe die besten Eigenschaften von Mockery hervor.
Mockery bietet eine Reihe spaßbezogener Matcher an, die einem Hamcrest-Wörterbuch sehr ähnlich sind und eine sehr natürliche Möglichkeit bieten, gemeckte Erwartungen auszudrücken. Mockery überschreibt nicht die in PHPUnit integrierten Spottfunktionen und steht nicht in Konflikt mit diesen. Sie können beide gleichzeitig (und sogar in derselben Testmethode) verwenden.
Mockery installieren
Es gibt mehrere Möglichkeiten, Mockery zu installieren. Hier sind die gängigsten Methoden.
Composer verwenden
Erstellen Sie eine Datei mit dem Namen composer.json im Stammordner Ihres Projekts, und fügen Sie der Datei den folgenden Code hinzu:
1 |
{
|
2 |
"require": { |
3 |
"Mockery/Mockery": ">=0.7.2" |
4 |
}
|
5 |
}
|
Als Nächstes installieren Sie Composer einfach mit dem folgenden Befehl im Stammordner Ihres Projekts:
1 |
curl -s http://getcomposer.org/installer | php
|
Installieren Sie abschließend alle erforderlichen Abhängigkeiten (einschließlich Mockery) mit diesem Befehl:
1 |
php composer.phar install
|
Wenn alles installiert ist, stellen wir sicher, dass unsere Mockery-Installation funktioniert. Der Einfachheit halber gehe ich davon aus, dass Sie im Stammverzeichnis Ihres Projekts einen Ordner namens Test haben. Alle Beispiele in diesem Lernprogramm befinden sich in diesem Ordner. Hier ist der Code, den ich verwendet habe, um sicherzustellen, dass Mockery mit meinem Projekt funktioniert:
1 |
//Filename: JustToCheckMockeryTest.php
|
2 |
|
3 |
require_once '../vendor/autoload.php'; |
4 |
|
5 |
class JustToCheckMockeryTest extends PHPUnit_Framework_TestCase { |
6 |
|
7 |
protected function tearDown() { |
8 |
\Mockery::close(); |
9 |
}
|
10 |
|
11 |
|
12 |
function testMockeryWorks() { |
13 |
$mock = \Mockery::mock('AClassToBeMocked'); |
14 |
$mock->shouldReceive('someMethod')->once(); |
15 |
|
16 |
$workerObject = new AClassToWorkWith; |
17 |
$workerObject->doSomethingWit($mock); |
18 |
}
|
19 |
}
|
20 |
|
21 |
class AClassToBeMocked {} |
22 |
|
23 |
class AClassToWorkWith { |
24 |
|
25 |
function doSomethingWit($anotherClass) { |
26 |
return $anotherClass->someMethod(); |
27 |
}
|
28 |
|
29 |
}
|
Linux-Benutzer: Verwenden Sie die Pakete Ihrer Distribution
Einige Linux-Distributionen erleichtern die Installation von Mockery, aber nur eine Handvoll stellt ein Mockery-Paket für ihr System bereit. Die folgende Liste ist die einzige, die mir bekannt ist:
- Sabayon:
equo install Mockery - Fedora / RHE:
yum install Mockery
Use PEAR
PEAR-Fans können Mockery mit den folgenden Befehlen installieren:
1 |
sudo pear channel-discover pear.survivethedeepend.com
|
2 |
sudo pear channel-discover hamcrest.googlecode.com/svn/pear
|
3 |
sudo pear install --alldeps deepend/Mockery |
Installation von der Quelle
Die Installation von GitHub ist für echte Geeks! Sie können die neueste Version von Mockery jederzeit über das GitHub-Repository abrufen.
1 |
git clone git://github.com/padraic/Mockery.git |
2 |
cd Mockery
|
3 |
sudo pear channel-discover hamcrest.googlecode.com/svn/pear
|
4 |
sudo pear install --alldeps package.xml |
Unser erstes verspottetes Objekt erstellen
Machen wir uns über einige Objekte lustig, bevor wir Erwartungen definieren. Mit dem folgenden Code wird das vorherige Beispiel so geändert, dass es sowohl Beispiele für PHPUnit als auch Mockery enthält:
1 |
//Filename: MockeryABetterWayOfMockingTest.php
|
2 |
require_once '../vendor/autoload.php'; |
3 |
|
4 |
class MockeryVersusPHPUnitGetMockTest extends PHPUnit_Framework_TestCase { |
5 |
|
6 |
function testCreateAMockedObject() { |
7 |
// With PHPUnit
|
8 |
$phpunitMock = $this->getMock('AClassToBeMocked'); |
9 |
|
10 |
// With Mockery
|
11 |
$mockeryMock = \Mockery::mock('AClassToBeMocked'); |
12 |
}
|
13 |
|
14 |
}
|
15 |
|
16 |
class AClassToBeMocked { |
17 |
|
18 |
}
|
Mit Mockery können Sie Mocks für Klassen definieren, die nicht vorhanden sind.
Die erste Zeile stellt sicher, dass wir Zugang zu Mockery haben. Als Nächstes erstellen wir eine Testklasse namens MockeryVersusPHPUnitGetMockTest, die die Methode testCreateAMockedObject() enthält. Die gespielte Klasse AClassToBeMocked ist zu diesem Zeitpunkt vollständig leer. Sie könnten die Klasse vollständig entfernen, ohne dass der Test fehlschlägt.
Die Testmethode testCreateAMockedObject() definiert zwei Objekte. Der erste ist ein PHPUnit-Mock, der zweite wird mit Mockery erstellt. Mockery's Syntax lautet:
1 |
$mockedObject = \Mockery::mock('SomeClassToBeMocked'); |
Weisen Sie einfache Erwartungen zu
Mocks werden im Allgemeinen verwendet, um das Verhalten eines Objekts (hauptsächlich seine Methoden) zu überprüfen, indem es sogenannte Erwartungen angibt. Lassen Sie uns ein paar einfache Erwartungen aufstellen.
Erwarten Sie, dass eine Methode aufgerufen wird
Die häufigste Erwartung ist wahrscheinlich eine, die einen bestimmten Methodenaufruf erwartet. Bei den meisten Mocking-Frameworks können Sie die Anzahl der Aufrufe angeben, die eine Methode erwarten soll. Wir beginnen mit einer einfachen Erwartung eines einzelnen Anrufs:
1 |
//Filename: MockeryABetterWayOfMockingTest.php
|
2 |
require_once '../vendor/autoload.php'; |
3 |
|
4 |
class MockeryVersusPHPUnitGetMockTest extends PHPUnit_Framework_TestCase { |
5 |
|
6 |
protected function tearDown() { |
7 |
\Mockery::close(); |
8 |
}
|
9 |
|
10 |
function testExpectOnce() { |
11 |
$someObject = new SomeClass(); |
12 |
|
13 |
// With PHPUnit
|
14 |
$phpunitMock = $this->getMock('AClassToBeMocked'); |
15 |
$phpunitMock->expects($this->once())->method('someMethod'); |
16 |
// Exercise for PHPUnit
|
17 |
$someObject->doSomething($phpunitMock); |
18 |
|
19 |
// With Mockery
|
20 |
$mockeryMock = \Mockery::mock('AnInexistentClass'); |
21 |
$mockeryMock->shouldReceive('someMethod')->once(); |
22 |
// Exercise for Mockery
|
23 |
$someObject->doSomething($mockeryMock); |
24 |
}
|
25 |
|
26 |
}
|
27 |
|
28 |
class AClassToBeMocked { |
29 |
function someMethod() {} |
30 |
}
|
31 |
|
32 |
class SomeClass { |
33 |
function doSomething($anotherObject) { |
34 |
$anotherObject->someMethod(); |
35 |
}
|
36 |
}
|
Dieser Code konfiguriert eine Erwartung sowohl für PHPUnit als auch für Mockery. Beginnen wir mit dem ersteren.
Einige Linux-Distributionen erleichtern die Installation von Mockery.
Wir verwenden die expects() -Methode, um eine Erwartung zu definieren, die someMethod() einmalig aufrufen soll. Damit PHPUnit jedoch ordnungsgemäß funktioniert, müssen wir eine Klasse namens AClassToBeMocked definieren und über eine someMethod () - Methode verfügen.
Das ist ein Problem. Wenn Sie viele Objekte verspotten und nach TDD-Prinzipien für ein Top-Down-Design entwickeln, möchten Sie nicht vor dem Test alle Klassen und Methoden erstellen. Ihr Test sollte aus dem richtigen Grund fehlschlagen, dass die erwartete Methode nicht aufgerufen wurde, anstatt einen kritischen PHP-Fehler ohne Bezug zur tatsächlichen Implementierung. Versuchen Sie, die someMethod() -Definition von AClassToBeMocked zu entfernen, und sehen Sie, was passiert.
Auf der anderen Seite können Sie mit Mockery Mock für Klassen definieren, die nicht vorhanden sind.
Beachten Sie, dass das obige Beispiel einen Anock für AnInexistentClass erstellt, der, wie der Name schon sagt, nicht existiert (und auch seine someMethod() - Methode).
Am Ende des obigen Beispiels definieren wir die SomeClass-Klasse, um unseren Code auszuführen. Wir initialisieren ein Objekt mit dem Namen $someObject in der ersten Zeile der Testmethode und üben den Code effektiv aus, nachdem wir unsere Erwartungen definiert haben.
Bitte beachten Sie: Mockery bewertet die Erwartungen an die close() - Methode. Aus diesem Grund sollten Sie immer eine tearDown() -Methode in Ihrem Test verwenden, die \Mockery::close() aufruft. Ansonsten gibt Mockery falsch positive Ergebnisse.
Erwarten Sie mehr als einen Anruf
Wie bereits erwähnt, können die meisten Mocking-Frameworks die Erwartungen für mehrere Methodenaufrufe angeben. PHPUnit verwendet zu diesem Zweck das Konstrukt $this->exact() .Der folgende Code definiert die Erwartungen für den mehrmaligen Aufruf einer Methode:
1 |
function testExpectMultiple() { |
2 |
$someObject = new SomeClass(); |
3 |
|
4 |
// With PHPUnit 2 times
|
5 |
$phpunitMock = $this->getMock('AClassToBeMocked'); |
6 |
$phpunitMock->expects($this->exactly(2))->method('someMethod'); |
7 |
// Exercise for PHPUnit
|
8 |
$someObject->doSomething($phpunitMock); |
9 |
$someObject->doSomething($phpunitMock); |
10 |
|
11 |
// With Mockery 2 times
|
12 |
$mockeryMock = \Mockery::mock('AnInexistentClass'); |
13 |
$mockeryMock->shouldReceive('someMethod')->twice(); |
14 |
// Exercise for Mockery
|
15 |
$someObject->doSomething($mockeryMock); |
16 |
$someObject->doSomething($mockeryMock); |
17 |
|
18 |
// With Mockery 3 times
|
19 |
$mockeryMock = \Mockery::mock('AnInexistentClass'); |
20 |
$mockeryMock->shouldReceive('someMethod')->times(3); |
21 |
// Exercise for Mockery
|
22 |
$someObject->doSomething($mockeryMock); |
23 |
$someObject->doSomething($mockeryMock); |
24 |
$someObject->doSomething($mockeryMock); |
25 |
}
|
Mockery bietet zwei verschiedene Methoden, um Ihre Anforderungen besser zu erfüllen. Die erste Methode,twice (), erwartet zwei Methodenaufrufe. Die andere Methode ist times(), mit der Sie einen Betrag angeben können. Mockery's Ansatz ist viel sauberer und leichter lesbar.
Werte zurückgeben
Eine weitere übliche Verwendung für Mocks besteht darin, den Rückgabewert einer Methode zu testen. Natürlich haben sowohl PHPUnit als auch Mockery die Mittel, um Rückgabewerte zu überprüfen. Lassen Sie uns noch einmal mit etwas Einfachem beginnen.
Einfache Rückgabewerte
Der folgende Code enthält sowohl PHPUnit- als auch Mockery-Code. Ich habe SomeClass auch aktualisiert, um einen testbaren Rückgabewert bereitzustellen.
1 |
class MockeryVersusPHPUnitGetMockTest extends PHPUnit_Framework_TestCase { |
2 |
|
3 |
protected function tearDown() { |
4 |
\Mockery::close(); |
5 |
}
|
6 |
|
7 |
// [...] //
|
8 |
|
9 |
function testSimpleReturnValue() { |
10 |
$someObject = new SomeClass(); |
11 |
$someValue = 'some value'; |
12 |
|
13 |
// With PHPUnit
|
14 |
$phpunitMock = $this->getMock('AClassToBeMocked'); |
15 |
$phpunitMock->expects($this->once())->method('someMethod')->will($this->returnValue($someValue)); |
16 |
// Expect the returned value
|
17 |
$this->assertEquals($someValue, $someObject->doSomething($phpunitMock)); |
18 |
|
19 |
|
20 |
// With Mockery
|
21 |
$mockeryMock = \Mockery::mock('AnInexistentClass'); |
22 |
$mockeryMock->shouldReceive('someMethod')->once()->andReturn($someValue); |
23 |
// Expect the returned value
|
24 |
$this->assertEquals($someValue, $someObject->doSomething($mockeryMock)); |
25 |
}
|
26 |
|
27 |
}
|
28 |
|
29 |
class AClassToBeMocked { |
30 |
|
31 |
function someMethod() { |
32 |
|
33 |
}
|
34 |
|
35 |
}
|
36 |
|
37 |
class SomeClass { |
38 |
|
39 |
function doSomething($anotherObject) { |
40 |
return $anotherObject->someMethod(); |
41 |
}
|
42 |
|
43 |
}
|
Die API von PHPUnit und Mockery ist unkompliziert und einfach zu bedienen, aber ich finde Mockery immer noch sauberer und lesbarer.
Verschiedene Werte zurückgeben
Häufige Unit-Tester können mit Methoden, die unterschiedliche Werte zurückgeben, Komplikationen feststellen. Leider ist die beschränkte $this->at($ index) -Methode von PHPUnit die einzige Möglichkeit, verschiedene Werte aus derselben Methode zurückzugeben. Der folgende Code demonstriert die at() -Methode:
1 |
function testDemonstratePHPUnitCallIndexing() { |
2 |
$someObject = new SomeClass(); |
3 |
$firstValue = 'first value'; |
4 |
$secondValue = 'second value'; |
5 |
|
6 |
// With PHPUnit
|
7 |
$phpunitMock = $this->getMock('AClassToBeMocked'); |
8 |
$phpunitMock->expects($this->at(0))->method('someMethod')->will($this->returnValue($firstValue)); |
9 |
$phpunitMock->expects($this->at(1))->method('someMethod')->will($this->returnValue($secondValue)); |
10 |
// Expect the returned value
|
11 |
$this->assertEquals($firstValue, $someObject->doSomething($phpunitMock)); |
12 |
$this->assertEquals($secondValue, $someObject->doSomething($phpunitMock)); |
13 |
|
14 |
}
|
Dieser Code definiert zwei separate Erwartungen und führt zwei verschiedene Aufrufe von someMethod() aus. also ist dieser test bestanden. Aber lassen Sie uns eine Wendung einführen und einen Doppelruf in der getesteten Klasse hinzufügen:
1 |
// [...] //
|
2 |
function testDemonstratePHPUnitCallIndexingOnTheSameClass() { |
3 |
$someObject = new SomeClass(); |
4 |
$firstValue = 'first value'; |
5 |
$secondValue = 'second value'; |
6 |
|
7 |
// With PHPUnit
|
8 |
$phpunitMock = $this->getMock('AClassToBeMocked'); |
9 |
$phpunitMock->expects($this->at(0))->method('someMethod')->will($this->returnValue($firstValue)); |
10 |
$phpunitMock->expects($this->at(1))->method('someMethod')->will($this->returnValue($secondValue)); |
11 |
// Expect the returned value
|
12 |
$this->assertEquals('first value second value', $someObject->concatenate($phpunitMock)); |
13 |
|
14 |
}
|
15 |
|
16 |
class SomeClass { |
17 |
|
18 |
function doSomething($anotherObject) { |
19 |
return $anotherObject->someMethod(); |
20 |
}
|
21 |
|
22 |
function concatenate($anotherObject) { |
23 |
return $anotherObject->someMethod() . ' ' . $anotherObject->someMethod(); |
24 |
}
|
25 |
|
26 |
}
|
Der Test ist immer noch bestanden. PHPUnit erwartet zwei Aufrufe von someMethod(), die in der getesteten Klasse auftreten, wenn die Verkettung über die concatenate() -Methode ausgeführt wird. Der erste Aufruf gibt den ersten Wert und der zweite Aufruf den zweiten Wert zurück. Aber hier ist der Haken: Was würde passieren, wenn Sie die Behauptung verdoppeln? Hier ist der Code:
1 |
function testDemonstratePHPUnitCallIndexingOnTheSameClass() { |
2 |
$someObject = new SomeClass(); |
3 |
$firstValue = 'first value'; |
4 |
$secondValue = 'second value'; |
5 |
|
6 |
// With PHPUnit
|
7 |
$phpunitMock = $this->getMock('AClassToBeMocked'); |
8 |
$phpunitMock->expects($this->at(0))->method('someMethod')->will($this->returnValue($firstValue)); |
9 |
$phpunitMock->expects($this->at(1))->method('someMethod')->will($this->returnValue($secondValue)); |
10 |
// Expect the returned value
|
11 |
$this->assertEquals('first value second value', $someObject->concatenate($phpunitMock)); |
12 |
$this->assertEquals('first value second value', $someObject->concatenate($phpunitMock)); |
13 |
|
14 |
}
|
Es gibt den folgenden Fehler zurück:
1 |
Failed asserting that two strings are equal. |
2 |
--- Expected |
3 |
+++ Actual |
4 |
@@ @@ |
5 |
-'first value second value' |
6 |
+' ' |
PHPUnit zählt weiterhin zwischen verschiedenen Aufrufen von concatenate(). Zum Zeitpunkt des zweiten Aufrufs in der letzten Assertion hat $index die Werte 2 und 3. Sie können den Test bestehen, indem Sie Ihre Erwartungen ändern, um die zwei neuen Schritte wie folgt zu berücksichtigen:
1 |
function testDemonstratePHPUnitCallIndexingOnTheSameClass() { |
2 |
$someObject = new SomeClass(); |
3 |
$firstValue = 'first value'; |
4 |
$secondValue = 'second value'; |
5 |
|
6 |
// With PHPUnit
|
7 |
$phpunitMock = $this->getMock('AClassToBeMocked'); |
8 |
$phpunitMock->expects($this->at(0))->method('someMethod')->will($this->returnValue($firstValue)); |
9 |
$phpunitMock->expects($this->at(1))->method('someMethod')->will($this->returnValue($secondValue)); |
10 |
$phpunitMock->expects($this->at(2))->method('someMethod')->will($this->returnValue($firstValue)); |
11 |
$phpunitMock->expects($this->at(3))->method('someMethod')->will($this->returnValue($secondValue)); |
12 |
// Expect the returned value
|
13 |
$this->assertEquals('first value second value', $someObject->concatenate($phpunitMock)); |
14 |
$this->assertEquals('first value second value', $someObject->concatenate($phpunitMock)); |
15 |
|
16 |
}
|
Sie können wahrscheinlich mit diesem Code leben, aber Mockery macht dieses Szenario trivial. Glaub mir nicht Schau mal:
1 |
function testMultipleReturnValuesWithMockery() { |
2 |
$someObject = new SomeClass(); |
3 |
$firstValue = 'first value'; |
4 |
$secondValue = 'second value'; |
5 |
|
6 |
// With Mockery
|
7 |
$mockeryMock = \Mockery::mock('AnInexistentClass'); |
8 |
$mockeryMock->shouldReceive('someMethod')->andReturn($firstValue, $secondValue, $firstValue, $secondValue); |
9 |
|
10 |
// Expect the returned value
|
11 |
$this->assertEquals('first value second value', $someObject->concatenate($mockeryMock)); |
12 |
$this->assertEquals('first value second value', $someObject->concatenate($mockeryMock)); |
13 |
}
|
Wie PHPUnit verwendet Mockery die Indexzählung, aber wir müssen uns keine Sorgen um Indizes machen. Stattdessen listen wir einfach alle erwarteten Werte auf und Mockery gibt sie der Reihe nach zurück.
Darüber hinaus gibt PHPUnit für nicht angegebene Indizes NULL zurück, Mockery gibt jedoch immer den zuletzt angegebenen Wert zurück. Das ist eine nette Geste.
Versuchen Sie mehrere Methoden mit der Indizierung
Lassen Sie uns eine zweite Methode in unseren Code einführen, die Methode concatWithMinus():
1 |
class SomeClass { |
2 |
|
3 |
function doSomething($anotherObject) { |
4 |
return $anotherObject->someMethod(); |
5 |
}
|
6 |
|
7 |
function concatenate($anotherObject) { |
8 |
return $anotherObject->someMethod() . ' ' . $anotherObject->someMethod(); |
9 |
}
|
10 |
|
11 |
function concatWithMinus($anotherObject) { |
12 |
return $anotherObject->anotherMethod() . ' - ' . $anotherObject->anotherMethod(); |
13 |
}
|
14 |
|
15 |
}
|
Diese Methode verhält sich ähnlich wie concatenate(), aber sie verknüpft die Zeichenfolgenwerte mit " - " im Gegensatz zu einem einzelnen Leerzeichen. Da diese beiden Methoden ähnliche Aufgaben ausführen, ist es sinnvoll, sie innerhalb derselben Testmethode zu testen, um doppelte Tests zu vermeiden.
Wie im obigen Code gezeigt, verwendet die zweite Funktion eine andere Methode mit dem Namen anotherMethod(). Ich habe diese Änderung vorgenommen, um uns zu zwingen, in unseren Tests beide Methoden zu verspotten. Unsere spottbare Klasse sieht jetzt so aus:
1 |
class AClassToBeMocked { |
2 |
|
3 |
function someMethod() { |
4 |
|
5 |
}
|
6 |
|
7 |
function anotherMethod() { |
8 |
|
9 |
}
|
10 |
|
11 |
}
|
Das Testen mit PHPUnit könnte folgendermaßen aussehen:
1 |
function testPHPUnitIndexingOnMultipleMethods() { |
2 |
$someObject = new SomeClass(); |
3 |
$firstValue = 'first value'; |
4 |
$secondValue = 'second value'; |
5 |
|
6 |
// With PHPUnit
|
7 |
$phpunitMock = $this->getMock('AClassToBeMocked'); |
8 |
|
9 |
// First and second call on the semeMethod:
|
10 |
$phpunitMock->expects($this->at(0))->method('someMethod')->will($this->returnValue($firstValue)); |
11 |
$phpunitMock->expects($this->at(1))->method('someMethod')->will($this->returnValue($secondValue)); |
12 |
// Expect the returned value
|
13 |
$this->assertEquals('first value second value', $someObject->concatenate($phpunitMock)); |
14 |
|
15 |
// First and second call on the anotherMethod:
|
16 |
$phpunitMock->expects($this->at(0))->method('anotherMethod')->will($this->returnValue($firstValue)); |
17 |
$phpunitMock->expects($this->at(1))->method('anotherMethod')->will($this->returnValue($secondValue)); |
18 |
// Expect the returned value
|
19 |
$this->assertEquals('first value - second value', $someObject->concatWithMinus($phpunitMock)); |
20 |
}
|
Die Logik ist solide. Definieren Sie für jede Methode zwei unterschiedliche Erwartungen und geben Sie den Rückgabewert an. Dies funktioniert nur mit PHPUnit 3.6 oder neuer.
Bitte beachten Sie: PHPunit 3.5 und älter hatten einen Fehler, durch den der Index nicht für jede Methode zurückgesetzt wurde. Dies führte zu unerwarteten Rückgabewerten für gemockte Methoden.
Schauen wir uns das gleiche Szenario mit Mockery an. Wieder erhalten wir viel saubereren Code. Überzeugen Sie sich selbst:
1 |
function testMultipleReturnValuesForDifferentFunctionsWithMockery() { |
2 |
$someObject = new SomeClass(); |
3 |
$firstValue = 'first value'; |
4 |
$secondValue = 'second value'; |
5 |
|
6 |
// With Mockery
|
7 |
$mockeryMock = \Mockery::mock('AnInexistentClass'); |
8 |
$mockeryMock->shouldReceive('someMethod')->andReturn($firstValue, $secondValue); |
9 |
$mockeryMock->shouldReceive('anotherMethod')->andReturn($firstValue, $secondValue); |
10 |
|
11 |
// Expect the returned value
|
12 |
$this->assertEquals('first value second value', $someObject->concatenate($mockeryMock)); |
13 |
$this->assertEquals('first value - second value', $someObject->concatWithMinus($mockeryMock)); |
14 |
}
|
Rückgabewerte basierend auf den angegebenen Parametern
Ehrlich gesagt, das kann PHPUnit einfach nicht. Zum Zeitpunkt dieses Schreibens erlaubt PHPUnit nicht, unterschiedliche Werte von derselben Funktion basierend auf dem Parameter der Funktion zurückzugeben. Daher schlägt der folgende Test fehl:
1 |
// [...] //
|
2 |
function testPHUnitCandDecideByParameter() { |
3 |
$someObject = new SomeClass(); |
4 |
|
5 |
// With PHPUnit
|
6 |
$phpunitMock = $this->getMock('AClassToBeMocked'); |
7 |
$phpunitMock->expects($this->any())->method('getNumber')->with(2)->will($this->returnValue(2)); |
8 |
$phpunitMock->expects($this->any())->method('getNumber')->with(3)->will($this->returnValue(3)); |
9 |
|
10 |
$this->assertEquals(4, $someObject->doubleNumber($phpunitMock, 2)); |
11 |
$this->assertEquals(6, $someObject->doubleNumber($phpunitMock, 3)); |
12 |
|
13 |
}
|
14 |
|
15 |
class AClassToBeMocked { |
16 |
|
17 |
// [...] //
|
18 |
function getNumber($number) { |
19 |
return $number; |
20 |
}
|
21 |
}
|
22 |
|
23 |
class SomeClass { |
24 |
|
25 |
// [...] //
|
26 |
|
27 |
function doubleNumber($anotherObject, $number) { |
28 |
return $anotherObject->getNumber($number) * 2; |
29 |
}
|
30 |
}
|
Bitte ignorieren Sie die Tatsache, dass es in diesem Beispiel keine Logik gibt. es würde auch scheitern, wenn es anwesend wäre. Dieser Code hilft jedoch, die Idee zu veranschaulichen.
Dieser Test schlägt fehl, da PHPUnit die beiden Erwartungen im Test nicht unterscheiden kann. Die zweite Erwartung, die Parameter 3 erwartet, überschreibt einfach den ersten erwarteten Parameter 2. Wenn Sie versuchen, diesen Test auszuführen, wird der folgende Fehler angezeigt:
1 |
Expectation failed for method name is equal to <string:getNumber> when invoked zero or more times
|
2 |
Parameter 0 for invocation AClassToBeMocked::getNumber(2) does not match expected value. |
3 |
Failed asserting that 2 matches expected 3. |
Mockery kann dies tun, und der folgende Code funktioniert genauso, wie Sie es erwarten würden. Die Methode gibt basierend auf den angegebenen Parametern unterschiedliche Werte zurück:
1 |
function testMockeryReturningDifferentValuesBasedOnParameter() { |
2 |
$someObject = new SomeClass(); |
3 |
|
4 |
// Mockery
|
5 |
$mockeryMock = \Mockery::mock('AnInexistentClass'); |
6 |
$mockeryMock->shouldReceive('getNumber')->with(2)->andReturn(2); |
7 |
$mockeryMock->shouldReceive('getNumber')->with(3)->andReturn(3); |
8 |
|
9 |
$this->assertEquals(4, $someObject->doubleNumber($mockeryMock, 2)); |
10 |
$this->assertEquals(6, $someObject->doubleNumber($mockeryMock, 3)); |
11 |
}
|
Teilspötter
Manchmal möchten Sie nur bestimmte Methoden für Ihr Objekt nachahmen (im Gegensatz zu einem ganzen Objekt). Die folgende Calculator-Klasse ist bereits vorhanden. Wir möchten nur bestimmte Methoden verspotten:
1 |
class Calculator { |
2 |
function add($firstNo, $secondNo) { |
3 |
return $firstNo + $secondNo; |
4 |
}
|
5 |
|
6 |
function subtract($firstNo, $secondNo) { |
7 |
return $firstNo - $secondNo; |
8 |
}
|
9 |
|
10 |
function multiply($value, $multiplier) { |
11 |
$newValue = 0; |
12 |
for($i=0;$i<$multiplier;$i++) |
13 |
$newValue = $this->add($newValue, $value); |
14 |
return $newValue; |
15 |
}
|
16 |
}
|
Diese Calculator-Klasse verfügt über drei Methoden: add(), subtract() und multiply(). Multiplikation verwendet eine Schleife, um die Multiplikation durchzuführen, indem add() für eine bestimmte Anzahl von Zeiten aufgerufen wird (z. B. ist 2 x 3 wirklich 2 + 2 + 2).
Nehmen wir an, wir wollen multiplicly() in totaler Isolation testen. Wir werden also add() nachahmen und auf spezifisches Verhalten in multiply() prüfen. Hier sind einige mögliche Tests:
1 |
function testPartialMocking() { |
2 |
$value = 3; |
3 |
$multiplier = 2; |
4 |
$result = 6; |
5 |
|
6 |
// PHPUnit
|
7 |
$phpMock = $this->getMock('Calculator', array('add')); |
8 |
$phpMock->expects($this->exactly(2))->method('add')->will($this->returnValue($result)); |
9 |
|
10 |
$this->assertEquals($result, $phpMock->multiply($value,$multiplier)); |
11 |
|
12 |
// Mockery
|
13 |
$mockeryMock = \Mockery::mock(new Calculator); |
14 |
$mockeryMock->shouldReceive('add')->andReturn($result); |
15 |
|
16 |
$this->assertEquals($result, $mockeryMock->multiply($value,$multiplier)); |
17 |
|
18 |
// Mockery extended test checking parameters
|
19 |
$mockeryMock2 = \Mockery::mock(new Calculator); |
20 |
$mockeryMock2->shouldReceive('add')->with(0,3)->andReturn(3); |
21 |
$mockeryMock2->shouldReceive('add')->with(3,3)->andReturn(6); |
22 |
|
23 |
$this->assertEquals($result, $mockeryMock2->multiply($value,$multiplier)); |
24 |
}
|
Spott bietet ... einen sehr natürlichen Weg, um verspottete Erwartungen auszudrücken.
Der erste PHPUnit-Test ist anämisch. es testet einfach, dass die Methode add() zweimal aufgerufen wird und gibt bei jedem Aufruf den endgültigen Wert zurück. Es erledigt die Arbeit, ist aber auch etwas kompliziert. PHPUnit zwingt Sie, die Liste der Methoden, die Sie als zweiten Parameter simulieren möchten, an $this->getMock() zu übergeben. Ansonsten würde PHPUnit alle Methoden verspotten, wobei jede standardmäßig NULL zurückgibt. Diese Liste muss mit den Erwartungen übereinstimmen, die Sie für Ihr Objekt festlegen.
Wenn ich zum Beispiel der substract() - Methode von $phpMock eine zweite Erwartung hinzufüge, würde PHPUnit dies ignorieren und die ursprüngliche substract() - Methode aufrufen. Das heißt, es sei denn, ich gebe explizit den Namen der Methode (substract) in der $this->getmock() - Anweisung an.
Mockery unterscheidet sich natürlich dadurch, dass Sie \Mockery::mock() ein reales Objekt zur Verfügung stellen können, und es erstellt automatisch einen partiellen Mock. Dies wird erreicht, indem eine Proxy-ähnliche Lösung für das Spotting implementiert wird. Alle Erwartungen, die Sie definieren, werden verwendet, aber Mockery greift auf die ursprüngliche Methode zurück, wenn Sie keine Erwartung für diese Methode angeben.
Bitte beachten Sie: Der Ansatz von Mockery ist sehr einfach, aber interne Methodenaufrufe durchlaufen das gespielte Objekt nicht.
Dieses Beispiel ist irreführend, aber es veranschaulicht, wie Mockery-Partial-Mocks nicht verwendet werden. Ja, Mockery erstellt einen partiellen Mock, wenn Sie ein reales Objekt übergeben, aber es werden nur externe Anrufe verspottet. Basierend auf dem vorherigen Code ruft die Methode multiply() beispielsweise die echte Methode add() auf. Versuchen Sie, die letzte Erwartung von ...->undReturn(6) in ...->andReturn(7) zu ändern. Der Test sollte offensichtlich fehlschlagen, aber dies ist nicht der Fall, da das echte add() anstelle der simulierten add() - Methode ausgeführt wird.
Wir können dieses Problem jedoch umgehen, indem Sie solche Muster wie folgt erstellen:
1 |
//Instead of
|
2 |
$mockeryMock = \Mockery::mock(new Calculator); |
3 |
// Create the mock like this
|
4 |
$mockeryMock = \Mockery::mock('Calculator[add]'); |
Das syntaktisch unterschiedliche Konzept ähnelt dem Ansatz von PHPUnit: Sie müssen die gespielten Methoden an zwei Stellen auflisten. Bei jedem anderen Test können Sie jedoch einfach das reale Objekt übergeben, was viel einfacher ist - insbesondere bei Konstruktorparametern.
Umgang mit Konstruktorparametern
Fügen wir der Klasse Calculator einen Konstruktor mit zwei Parametern hinzu. Der überarbeitete Code:
1 |
class Calculator { |
2 |
public $myNumbers = array(); |
3 |
|
4 |
function __construct($firstNo, $secondNo) { |
5 |
$this->myNumbers[]=$firstNo; |
6 |
$this->myNumbers[]=$secondNo; |
7 |
}
|
8 |
// [...] //
|
9 |
}
|
Jeder Test in diesem Artikel schlägt fehl, nachdem dieser Konstruktor hinzugefügt wurde. Genauer gesagt, führt der Test testPartialMock() zu folgendem Fehler:
1 |
Missing argument 1 for Calculator::__construct(), |
2 |
called in /usr/share/php/PHPUnit/Framework/MockObject/Generator.php |
3 |
on line 224 and defined |
PHPUnit versucht, das reale Objekt zu verspotten, indem es den Konstruktor automatisch aufruft und erwartet, dass die Parameter richtig eingestellt sind. Es gibt zwei Möglichkeiten, dieses Problem zu umgehen: Legen Sie entweder die Parameter fest oder rufen Sie den Konstruktor nicht auf.
1 |
//Specify Constructor Parameters
|
2 |
$phpMock = $this->getMock('Calculator', array('add'), array(1,2)); |
3 |
|
4 |
//Do not call original constructor
|
5 |
$phpMock = $this->getMock('Calculator', array('add'), array(), '', false); |
Spott geht automatisch um dieses Problem herum. Es ist in Ordnung, keinen Konstruktorparameter anzugeben. Mockery ruft den Konstruktor einfach nicht auf. Sie können jedoch eine Liste von Konstruktorparametern angeben, die von Mockery verwendet werden sollen. Zum Beispiel:
1 |
function testMockeryConstructorParameters() { |
2 |
$result = 6; |
3 |
// Mockery
|
4 |
// Do not call constructor
|
5 |
$noConstrucCall = \Mockery::mock('Calculator[add]'); |
6 |
$noConstrucCall->shouldReceive('add')->andReturn($result); |
7 |
|
8 |
// Use constructor parameters
|
9 |
$withConstructParams = \Mockery::mock('Calculator[add]', array(1,2)); |
10 |
$withConstructParams->shouldReceive('add')->andReturn($result); |
11 |
|
12 |
// User real object with real values and mock over it
|
13 |
$realCalculator = new Calculator(1,2); |
14 |
$mockRealObj = \Mockery::mock($realCalculator); |
15 |
$mockRealObj->shouldReceive('add')->andReturn($result); |
16 |
}
|
Technische Überlegungen
Mockery ist eine andere Bibliothek, in die Ihre Tests integriert werden. Vielleicht möchten Sie darüber nachdenken, welche technischen Auswirkungen dies haben kann.
- Spott braucht viel Speicher. Sie müssen den maximalen Speicher auf 512 MB erhöhen, wenn Sie viele Tests ausführen möchten (z. B. über 1000 Tests mit mehr als 3000 Assertions). Weitere Informationen finden Sie in der Dokumentation zur
php.ini. - Sie müssen Ihre Tests so organisieren, dass sie in separaten Prozessen ausgeführt werden, wenn statische Methoden und statische Methodenaufrufe verspottet werden.
- Sie können Mockery automatisch in jeden Test laden, indem Sie die Bootstrap-Funktionalität von PHPUnit verwenden (hilfreich, wenn Sie viele Tests haben und sich nicht wiederholen möchten).
- Sie können den Aufruf von
\Mockery::close()intearDown()jedes Tests automatisieren, indem Siephpunit.xmlbearbeiten.
Schlußfolgerungen
PHPUnit hat sicherlich seine Probleme, vor allem wenn es um Funktionalität und Ausdruckskraft geht. Spott kann Ihre Spott-Erfahrung erheblich verbessern, indem Sie das Schreiben und Verstehen von Tests vereinfachen - aber es ist nicht perfekt (es gibt nichts dergleichen).
Dieses Tutorial hat viele wichtige Aspekte von Mockery hervorgehoben, aber ehrlich gesagt haben wir die Oberfläche kaum zerkratzt. Machen Sie sich mit dem Github-Repository des Projekts vertraut, um mehr zu erfahren.
Danke fürs Lesen!



