1. Code
  2. PHP

Spott: Ein besserer Weg

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.
Scroll to top
16 min read
This post is part of a series called Test-Driven PHP.
Evolving Toward a Persistence Layer

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() in tearDown() jedes Tests automatisieren, indem Sie phpunit.xml bearbeiten.

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!