Erwartete Exceptions richtig testen

Der klassische Ablauf beim Testen von Code, der eine Exception werfen soll, ist der Folgende (PHPUnit):

/**  * @expectedException InvalidArgumentException  */ public function testException() {    throw new InvalidArgumentException(); } 

Problem dabei: Wir haben nicht spezifiziert, an welcher Stelle die Exception geworfen werden soll. Außerdem können wir nicht prüfen, ob die geworfene Exception genau die erwartete oder nur igendeine war.

Jetzt lässt sich das noch aufbohren:

/**  * @expectedException        InvalidArgumentException  * @expectedExceptionMessage Right Message  */ public function testExceptionHasRightMessage() {     throw new InvalidArgumentException('Right Message'); } 

Auch damit werde ich nicht glücklich. Wenn ich jetzt z.B. mehrere Exceptions in einem Test prüfen möchte (guter Stil hin oder her) stößt man an die Grenzen diesen Ansatzes.

Etwas feingranularer ist das Handling mit der nachfolgend vorstellten Methode setExpectedException.

Alternative Methode

public function testExceptionHasRightMessage() {     $this->setExpectedException(       'InvalidArgumentException', 'Right Message'     ); 	     throw new InvalidArgumentException('Right Message'); } 

Macht letztlich einen entscheidenden Unterschied verglichen mit der Variante per Annotation: Ich kann den Zeitpunkt selbst bestimmen, ab dem ich eine Exception erwarte. Die Annotation greift direkt ab der ersten Zeile der Methode, in der ich vielleicht noch garkeine Exception haben möchte.

Weils so schön simpel ist: Das macht setExpectedException under the hood:

public function setExpectedException($exceptionName, $exceptionMessage = '', $exceptionCode = 0) {     if ($exceptionName == 'Exception') { 	throw new InvalidArgumentException( 	  'You must not expect the generic exception class.'   	);     }      $this->expectedException        = $exceptionName;     $this->expectedExceptionMessage = $exceptionMessage;     $this->expectedExceptionCode    = $exceptionCode;     $this->expectedExceptionTrace   = debug_backtrace(); } 

… und wenn dann eine Exception fliegt, prüft das PHP Unit folgendermaßen ab:

try { 	$testResult = $method->invokeArgs( 	      $this, array_merge($this->data, $this->dependencyInput) 	); }  catch (Exception $e) { 	if (!$e instanceof PHPUnit_Framework_IncompleteTest && 		!$e instanceof PHPUnit_Framework_SkippedTest && 		is_string($this->expectedException) && 		$e instanceof $this->expectedException) { 		if (is_string($this->expectedExceptionMessage) && 			!empty($this->expectedExceptionMessage)) { 			$this->assertContains( 			  $this->expectedExceptionMessage, 			  $e->getMessage() 			); 		}        // ... } 

Volle Kontrolle!

Auch wenn das Testen mehrerer Exceptions in einer Methode durchaus umstritten ist, gibt es nun noch ein weiteres, gern genutztes Pattern, das eben dies ermöglicht:

try {     code();     $this->fail("No Exception was thrown"); } catch (InvalidArgumentException $ex) {     $this->assertEquals($ex->getMessage(), "Expected Exception-Text", "Wrong exceptiontext..."); } catch (Exception $ex) {     $this->fail("Wrong Exception was thrown"); } 

Muss natürlich für maximalen Komfort noch ausgebaut werden, ihr versteht worauf ich hinauswill.

Um das etwas komfortabler und wiederholungsfreier zu gestalten, hat dieser Herr eine Erweiterung zu PHPUnit geschrieben, womit uns seine Methode assertThrowsException die Arbeit abnimmt und den zu testenden Code in einer anonymen Funktion kapselt. Fühlt sich für mich am sympathischsten an.

<?php public function testSomeImportantMethod() {     $someClass = new SomeClass();      $this->assertThrowsException('InvalidArgumentException', function () use($someClass) {             $someClass->someMethod();         }     ); } 

Update: Danke an Thomas für eine Richtigstellung, habe den Artikel entsprechend angepasst.