Weiß jemand, ob es eine assert
oder etwas Ähnliches, das prüfen kann, ob im getesteten Code eine Ausnahme ausgelöst wurde?
Antworten
Zu viele Anzeigen?<?php
require_once 'PHPUnit/Framework.php';
class ExceptionTest extends PHPUnit_Framework_TestCase
{
public function testException()
{
$this->expectException(InvalidArgumentException::class);
// or for PHPUnit < 5.2
// $this->setExpectedException(InvalidArgumentException::class);
//...and then add your test code that generates the exception
exampleMethod($anInvalidArgument);
}
}
expectException() PHPUnit-Dokumentation
PHPUnit-Autorenartikel bietet ausführliche Erläuterungen zu den besten Praktiken für Testausnahmen.
Sie können auch eine Docblock-Anmerkung bis PHPUnit 9 veröffentlicht wird:
class ExceptionTest extends PHPUnit_Framework_TestCase
{
/**
* @expectedException InvalidArgumentException
*/
public function testException()
{
...
}
}
Für PHP 5.5+ (besonders mit Namespaced Code) bevorzuge ich jetzt die Verwendung von ::class
TLDR; scrollen Sie zu: Verwenden Sie den PHPUnit-Datenprovider
PHPUnit 9.5 bietet folgende Methoden zum Testen von Exceptions:
$this->expectException(string $exceptionClassName);
$this->expectExceptionCode(int|string $code);
$this->expectExceptionMessage(string $message);
$this->expectExceptionMessageMatches(string $regularExpression);
$this->expectExceptionObject(\Exception $exceptionObject);
Doch die Dokumentation die Reihenfolge der oben genannten Methoden im Prüfcode nicht klar ist.
Wenn Sie sich zum Beispiel an die Verwendung von Assertions gewöhnen:
<?php
class SimpleAssertionTest extends \PHPUnit\Framework\TestCase
{
public function testSimpleAssertion(): void
{
$expected = 'bar';
$actual = 'bar';
$this->assertSame($expected, $actual);
}
}
Ausgabe:
Simple assertion
OK (1 test, 1 assertion)
können Sie überrascht sein, wenn Sie den Ausnahmetest nicht bestehen:
<?php
use PHPUnit\Framework\TestCase;
final class ExceptionTest extends TestCase
{
public function testException(): void
{
throw new \InvalidArgumentException();
$this->expectException(\InvalidArgumentException::class);
}
}
Ausgabe:
Exception
InvalidArgumentException:
ERRORS!
Tests: 1, Assertions: 0, Errors: 1.
Der Fehler liegt darin begründet:
Sobald eine Ausnahme ausgelöst wird, kann PHP nicht mehr zu der Codezeile zurückkehren, die nach der Zeile kommt, die die Ausnahme ausgelöst hat. Das Auffangen einer Exception ändert nichts an dieser Tatsache. Das Auslösen einer Exception ist eine Einwegkarte.
Im Gegensatz zu Fehlern haben Exceptions keine Möglichkeit, sich von ihnen zu erholen und PHP dazu zu bringen, mit der Codeausführung fortzufahren, als ob es gar keine Ausnahme gegeben hätte.
Daher erreicht PHPUnit den Ort nicht einmal:
$this->expectException(\InvalidArgumentException::class);
wenn ihr ein anderer Name vorausging:
throw new \InvalidArgumentException();
Außerdem wird PHPUnit niemals in der Lage sein, diesen Ort zu erreichen, egal wie gut es Ausnahmen abfangen kann.
Verwenden Sie daher eine der Ausnahmetestmethoden von PHPUnit:
$this->expectException(string $exceptionClassName);
$this->expectExceptionCode(int|string $code);
$this->expectExceptionMessage(string $message);
$this->expectExceptionMessageMatches(string $regularExpression);
$this->expectExceptionObject(\Exception $exceptionObject);
muss sein vor einen Code, bei dem eine Ausnahme erwartet wird, die im Gegensatz zu einer Behauptung steht, die après ein aktueller Wert eingestellt wird.
Die richtige Reihenfolge bei der Verwendung von Ausnahmetests:
<?php
use PHPUnit\Framework\TestCase;
final class ExceptionTest extends TestCase
{
public function testException(): void
{
$this->expectException(\InvalidArgumentException::class);
throw new \InvalidArgumentException();
}
}
Da der Aufruf von PHPUnit-internen Methoden zum Testen von Exceptions erfolgen muss, bevor eine Exception ausgelöst wird, ist es sinnvoll, dass PHPUnit-Methoden, die sich auf Test-Exceptions beziehen, von $this->excpect
anstelle von $this->assert
.
Das wissen wir bereits:
Sobald eine Ausnahme ausgelöst wird, kann PHP nicht mehr zu der Codezeile zurückkehren, die nach der Zeile kommt, die die Ausnahme ausgelöst hat.
Sie sollten in der Lage sein, einen Fehler in diesem Test leicht zu erkennen:
<?php
namespace VendorName\PackageName;
class ExceptionTest extends \PHPUnit\Framework\TestCase
{
public function testThrowException(): void
{
# Should be OK
$this->expectException(\RuntimeException::class);
throw new \RuntimeException();
# Should Fail
$this->expectException(\RuntimeException::class);
throw new \InvalidArgumentException();
}
}
Die erste $this->expectException()
sollte in Ordnung sein, es erwartet eine Ausnahmeklasse, bevor eine genaue Ausnahmeklasse wie erwartet geworfen wird, also nichts falsch hier.
Die zweite, die ausfallen sollte, erwartet RuntimeException
Klasse, bevor eine ganz andere Ausnahme ausgelöst wird, so dass sie fehlschlagen sollte, aber wird die PHPUnit-Ausführung diese Stelle überhaupt erreichen?
Die Ausgabe des Tests ist:
Throw exception
OK (1 test, 1 assertion)
OK
?
Nein, sie ist weit davon entfernt OK
wenn der Test bestanden wird, und das sollte er Fail
bei der zweiten Ausnahme. Warum ist das so?
Beachten Sie, dass die Ausgabe hat:
OK (1 Test, 1 Behauptung)
wo die Anzahl der Tests richtig ist, aber es gibt nur 1 assertion
.
Es sollten 2 Behauptungen aufgestellt werden = OK
y Fail
die dazu führt, dass der Test nicht bestanden wird.
Das liegt einfach daran, dass PHPUnit mit der Ausführung von testThrowException
nach der Zeile:
throw new \RuntimeException();
das ist eine einfache Fahrkarte außerhalb des Geltungsbereichs der testThrowException
an eine Stelle, an der PHPUnit die \RuntimeException
und tut, was es tun muss, aber was auch immer es tun könnte, wir wissen, dass es nicht in der Lage sein wird, zurück in die testThrowException
daher der Code:
# Should Fail
$this->expectException(\RuntimeException::class);
throw new \InvalidArgumentException();
wird nie ausgeführt und deshalb ist das Testergebnis aus Sicht von PHPUnit OK
anstelle von Fail
.
Das ist keine gute Nachricht, wenn Sie mehrere $this->expectException()
oder eine Mischung aus $this->expectException()
y $this->expectExceptionMessage()
Aufrufe in derselben Testmethode:
<?php
namespace VendorName\PackageName;
class ExceptionTest extends \PHPUnit\Framework\TestCase
{
public function testThrowException(): void
{
# OK
$this->expectException(\RuntimeException::class);
throw new \RuntimeException('Something went wrong');
# Fail
$this->expectExceptionMessage('This code will never be executed');
throw new \RuntimeException('Something went wrong');
}
}
gibt falsch:
OK (1 Test, 1 Behauptung)
denn sobald eine Ausnahme ausgelöst wird, werden alle anderen $this->expect...
Aufrufe, die sich auf Testausnahmen beziehen, werden nicht ausgeführt und das Ergebnis des PHPUnit-Testfalls enthält nur das Ergebnis der ersten erwarteten Ausnahme.
Wie testet man mehrere Ausnahmen?
Mehrere Ausnahmen in separate Tests aufteilen:
<?php
namespace VendorName\PackageName;
class ExceptionTest extends \PHPUnit\Framework\TestCase
{
public function testThrowExceptionBar(): void
{
# OK
$this->expectException(\RuntimeException::class);
throw new \RuntimeException();
}
public function testThrowExceptionFoo(): void
{
# Fail
$this->expectException(\RuntimeException::class);
throw new \InvalidArgumentException();
}
}
gibt:
Throw exception bar
Throw exception foo
Failed aserting that exception of type "InvalidArgumentException" matches expected exception "RuntimeException". Message was: "" at
FAILURES!
Tests: 2, Assertions: 2, Failures: 1.
FAILURES
wie es sein sollte.
Diese Methode hat jedoch einen grundsätzlichen Nachteil - für jede geworfene Ausnahme ist ein eigener Test erforderlich. Das führt zu einer Flut von Tests, nur um Ausnahmen zu überprüfen.
Eine Ausnahme abfangen und mit einer Behauptung überprüfen
Wenn Sie die Skriptausführung nicht fortsetzen können, nachdem eine Ausnahme ausgelöst wurde, können Sie einfach eine erwartete Ausnahme abfangen und später mit den Methoden, die eine Ausnahme zur Verfügung stellt, alle Daten über sie erhalten und diese mit einer Kombination aus erwarteten Werten und Assertions verwenden:
<?php
namespace VendorName\PackageName;
class ExceptionTest extends \PHPUnit\Framework\TestCase
{
public function testThrowException(): void
{
# OK
unset($className);
try {
$location = __FILE__ . ':' . (string) (__LINE__ + 1);
throw new \RuntimeException('Something went wrong');
} catch (\Exception $e) {
$className = get_class($e);
$msg = $e->getMessage();
$code = $e->getCode();
}
$expectedClass = \RuntimeException::class;
$expectedMsg = 'Something went wrong';
$expectedCode = 0;
if (empty($className)) {
$failMsg = 'Exception: ' . $expectedClass;
$failMsg .= ' with msg: ' . $expectedMsg;
$failMsg .= ' and code: ' . $expectedCode;
$failMsg .= ' at: ' . $location;
$failMsg .= ' Not Thrown!';
$this->fail($failMsg);
}
$this->assertSame($expectedClass, $className);
$this->assertSame($expectedMsg, $msg);
$this->assertSame($expectedCode, $code);
# ------------------------------------------
# Fail
unset($className);
try {
$location = __FILE__ . ':' . (string) (__LINE__ + 1);
throw new \InvalidArgumentException('I MUST FAIL !');
} catch (\Exception $e) {
$className = get_class($e);
$msg = $e->getMessage();
$code = $e->getCode();
}
$expectedClass = \InvalidArgumentException::class;
$expectedMsg = 'Something went wrong';
$expectedCode = 0;
if (empty($className)) {
$failMsg = 'Exception: ' . $expectedClass;
$failMsg .= ' with msg: ' . $expectedMsg;
$failMsg .= ' and code: ' . $expectedCode;
$failMsg .= ' at: ' . $location;
$failMsg .= ' Not Thrown!';
$this->fail($failMsg);
}
$this->assertSame($expectedClass, $className);
$this->assertSame($expectedMsg, $msg);
$this->assertSame($expectedCode, $code);
}
}
gibt:
Throw exception
Failed asserting that two strings are identical.
---·Expected
+++·Actual
@@ @@
-'Something·went·wrong'
+'I·MUST·FAIL·!'
FAILURES!
Tests: 1, Assertions: 5, Failures: 1.
FAILURES
wie es sein sollte, aber oh mein Gott, haben Sie das alles oben gelesen? Sie müssen sich um die Klärung von Variablen kümmern unset($className);
um festzustellen, ob eine Ausnahme ausgelöst wurde, dann diese Kreatur $location = __FILE__ ...
um den genauen Ort der Ausnahme zu ermitteln, falls sie nicht ausgelöst wurde, und dann zu prüfen, ob die Ausnahme ausgelöst wurde if (empty($className)) { ... }
und mit $this->fail($failMsg);
zu signalisieren, wenn die Ausnahme nicht ausgelöst wurde.
Verwenden Sie den PHPUnit-Datenprovider
PHPUnit hat einen hilfreichen Mechanismus namens Datenanbieter . Ein Datenanbieter ist eine Methode, die die Daten (Array) mit Datensätzen zurückgibt. Ein einzelner Datensatz wird als Argument(e) verwendet, wenn eine Testmethode - testThrowException
wird von PHPUnit aufgerufen.
Wenn der Datenanbieter mehr als einen Datensatz zurückgibt, wird die Die Testmethode wird mehrfach ausgeführt , jedes Mal mit einem anderen Datensatz. Das ist hilfreich, wenn man mehrere Ausnahmen oder/und mehrere Ausnahmeneigenschaften wie Klassenname, Nachricht, Code testet, denn auch wenn:
Sobald eine Ausnahme ausgelöst wird, kann PHP nicht mehr zu der Codezeile zurückkehren, die nach der Zeile kommt, die die Ausnahme ausgelöst hat.
PHPUnit führt die Testmethode mehrfach aus, jedes Mal mit einem anderen Datensatz, so dass zum Beispiel mehrere Ausnahmen in einer einzigen Testmethode getestet werden (die fehlschlagen werden).
Deshalb können wir eine Testmethode für das Testen einer einzigen Ausnahme verantwortlich machen, aber diese Testmethode mehrmals mit verschiedenen Eingabedaten und erwarteten Ausnahmen ausführen, indem wir den PHPUnit-Datenanbieter verwenden.
Die Definition der Data-Provider-Methode kann wie folgt vorgenommen werden @dataProvider
Anmerkung zu der Testmethode, die vom Datenanbieter mit einem Datensatz geliefert werden sollte.
<?php
class ExceptionCheck
{
public function throwE($data)
{
if ($data === 1) {
throw new \RuntimeException;
} else {
throw new \InvalidArgumentException;
}
}
}
class ExceptionTest extends \PHPUnit\Framework\TestCase
{
public function ExceptionTestProvider() : array
{
$data = [
\RuntimeException::class =>
[
[
'input' => 1,
'className' => \RuntimeException::class
]
],
\InvalidArgumentException::class =>
[
[
'input' => 2,
'className' => \InvalidArgumentException::class
]
]
];
return $data;
}
/**
* @dataProvider ExceptionTestProvider
*/
public function testThrowException($data): void
{
$this->expectException($data['className']);
$exceptionCheck = new ExceptionCheck;
$exceptionCheck->throwE($data['input']);
}
}
ergibt das Ergebnis:
Throw exception with RuntimeException
Throw exception with InvalidArgumentException
OK (2 tests, 2 assertions)
Beachten Sie, dass es nur eine einzige Testmethode in der gesamten ExceptionTest
ist die Ausgabe von PHPUnit:
OK ( 2 Tests, 2 Behauptungen)
Also auch die Linie:
$exceptionCheck->throwE($data['input']);
die Ausnahme beim ersten Mal auslöste, war das kein Problem für das Testen einer weiteren Ausnahme mit derselben Testmethode, da PHPUnit sie dank des Datenanbieters mit einem anderen Datensatz erneut ausführte.
Jeder vom Datenanbieter zurückgegebene Datensatz kann benannt werden, Sie müssen lediglich einen String als Schlüssel verwenden, unter dem ein Datensatz gespeichert wird. Daher wird der erwartete Name der Ausnahmeklasse zweimal verwendet. Als Schlüssel des Datensatz-Arrays und als Wert (unter dem Schlüssel 'className'), der später als Argument für $this->expectException()
.
Die Verwendung von Zeichenketten als Schlüsselnamen für Datensätze macht diese Zusammenfassung hübsch und selbsterklärend:
Ausnahme auslösen mit RuntimeException
Ausnahme auslösen mit InvalidArgumentException
und wenn Sie die Zeile ändern:
if ($data === 1) {
zu:
if ($data !== 1) {
der public function throwE($data)
um falsche Ausnahmen auszulösen, und führen Sie PHPUnit erneut aus, dann werden Sie sehen:
Throw exception with RuntimeException
Faled asserting that exception of type "InvalidArgumentException" matches expected exception "RuntimeException". Message was: "" at (...)
Throw exception with InvalidArgumentException
Failed asserting that exception of type "RuntimeException" matches expected exception "InvalidArgumentException". Message was: "" at (...)
FAILURES!
Tests: 2, Assertions: 2, Failures: 2.
wie erwartet:
PLEITEN, PECH UND PANNEN! Tests: 2, Behauptungen: 2, Misserfolge: 2.
mit dem genauen Hinweis auf die Namen der Datensätze, die einige Probleme verursachten:
Ausnahme auslösen mit RuntimeException
Ausnahme auslösen mit InvalidArgumentException
Herstellung public function throwE($data)
keine Ausnahmen auslösen:
public function throwE($data)
{
}
und die erneute Ausführung von PHPUnit ergibt:
Throw exception with RuntimeException
Failed asserting that exception of type "RuntimeException" is thrown.
Throw exception with InvalidArgumentException
Failed asserting that exception of type "InvalidArgumentException" is thrown.
FAILURES!
Tests: 2, Assertions: 2, Failures: 2.
Die Verwendung eines Datenanbieters hat offenbar mehrere Vorteile:
- Die Eingabedaten und/oder erwarteten Daten sind von der eigentlichen Prüfmethode getrennt.
- Jeder Datensatz kann einen beschreibenden Namen haben, der deutlich macht, welcher Datensatz den Test bestanden oder nicht bestanden hat.
- Wenn ein Test fehlschlägt, erhalten Sie eine ordnungsgemäße Fehlermeldung, die besagt, dass eine Ausnahme nicht ausgelöst wurde oder eine falsche Ausnahme ausgelöst wurde, statt der Behauptung, dass x nicht y ist.
- Zum Testen einer einzelnen Methode, die mehrere Ausnahmen auslösen kann, ist nur eine einzige Testmethode erforderlich.
- Es ist möglich, mehrere Ausnahmen und/oder mehrere Ausnahmeneigenschaften wie Klassenname, Nachricht, Code zu testen.
- Es ist kein unwesentlicher Code wie ein try-catch-Block erforderlich, stattdessen wird einfach die eingebaute PHPUnit-Funktion verwendet.
Testen von Ausnahmen Gotchas
Ausnahme vom Typ "TypeError"
Mit PHP7 Datentyp Unterstützung dieser Test:
<?php
declare(strict_types=1);
class DatatypeChat
{
public function say(string $msg)
{
if (!is_string($msg)) {
throw new \InvalidArgumentException('Message must be a string');
}
return "Hello $msg";
}
}
class ExceptionTest extends \PHPUnit\Framework\TestCase
{
public function testSay(): void
{
$this->expectException(\InvalidArgumentException::class);
$chat = new DatatypeChat;
$chat->say(array());
}
}
schlägt bei der Ausgabe fehl:
Say
Failed asserting that exception of type "TypeError" matches expected exception "InvalidArgumentException". Message was: "Argument 1 passed to DatatypeChat::say() must be of the type string, array given (..)
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.
obwohl es in der Methode say
:
if (!is_string($msg)) {
throw new \InvalidArgumentException('Message must be a string');
}
und der Test übergibt ein Array anstelle eines Strings:
$chat->say(array());
PHP kann den Code nicht erreichen:
throw new \InvalidArgumentException('Message must be a string');
weil die Ausnahme aufgrund der Typisierung früher ausgelöst wird string
:
public function say(string $msg)
daher die TypeError
geworfen wird, anstatt InvalidArgumentException
Ausnahme vom Typ "TypeError" erneut
Wir wissen, dass wir keine if (!is_string($msg))
für die Überprüfung des Datentyps, da PHP sich bereits darum kümmert, wenn wir den Datentyp in der Methodendeklaration angeben say(string $msg)
wollen wir vielleicht InvalidArgumentException
wenn die Nachricht zu lang ist if (strlen($msg) > 3)
.
<?php
declare(strict_types=1);
class DatatypeChat
{
public function say(string $msg)
{
if (strlen($msg) > 3) {
throw new \InvalidArgumentException('Message is too long');
}
return "Hello $msg";
}
}
class ExceptionTest extends \PHPUnit\Framework\TestCase
{
public function testSayTooLong(): void
{
$this->expectException(\Exception::class);
$chat = new DatatypeChat;
$chat->say('I have more than 3 chars');
}
public function testSayDataType(): void
{
$this->expectException(\Exception::class);
$chat = new DatatypeChat;
$chat->say(array());
}
}
Ändern auch ExceptionTest
Wir haben also zwei Fälle (Testmethoden), in denen ein Exception
geworfen werden sollte - zuerst testSayTooLong
wenn die Nachricht zu lang ist und zweitens testSayDataType
wenn die Nachricht einen falschen Typ hat.
In beiden Tests erwarten wir anstelle einer spezifischen Ausnahmeklasse wie InvalidArgumentException
ou TypeError
nur eine Gattungsbezeichnung Exception
Klasse durch Verwendung von
$this->expectException(\Exception::class);
ist das Testergebnis:
Say too long
Say data type
Failed asserting that exception of type "TypeError" matches expected exception "Exception". Message was: "Argument 1 passed to DatatypeChat::say() must be of the type string, array given (..)
FAILURES!
Tests: 2, Assertions: 2, Failures: 1.
testSayTooLong()
in der Erwartung einer generischen Exception
und mit
$this->expectException(\Exception::class);
geht mit OK
wenn die InvalidArgumentException
wird geworfen
sondern
testSayDataType()
unter Verwendung der gleichen $this->expectException(\Exception::class);
Fails
mit der Beschreibung:
Mit der Behauptung gescheitert, dass Ausnahme des Typs " TypeError " entspricht der erwarteten Ausnahme " Ausnahme ".
Es sieht verwirrend aus, dass PHPUnit sich beschwert, dass Ausnahme TypeError
war kein Exception
sonst hätte es keine Probleme mit $this->expectException(\Exception::class);
innerhalb der testSayDataType()
da es keine Probleme mit dem testSayTooLong()
Werfen InvalidArgumentException
und erwartungsvoll: $this->expectException(\Exception::class);
Das Problem ist, dass PHPUnit Sie mit der obigen Beschreibung in die Irre führt, weil TypeError
es nicht eine Ausnahme. TypeError
erstreckt sich nicht auf die Exception
Klasse noch von einer anderen ihrer Kinder.
TypeError
implementiert Throwable
Schnittstelle siehe Dokumentation
während
InvalidArgumentException
erweitert LogicException
Dokumentation
et LogicException
erweitert Exception
Dokumentation
also InvalidArgumentException
erweitert Exception
auch.
Das ist der Grund, warum das Werfen der InvalidArgumentException
besteht den Test mit OK und $this->expectException(\Exception::class);
aber das Werfen TypeError
wird nicht (es erstreckt sich nicht auf Exception
)
Doch beide Exception
y TypeError
implementieren Throwable
Schnittstelle.
Daher ändern sich in beiden Tests
$this->expectException(\Exception::class);
zu
$this->expectException(\Throwable::class);
macht den Test grün:
Say too long
Say data type
OK (2 tests, 2 assertions)
Sehen Sie sich die Liste der Fehler- und Ausnahmeklassen an und wie sie miteinander verbunden sind .
Nur um das klarzustellen: es ist eine gute Praxis, eine spezifische Ausnahme oder einen Fehler für Unit-Tests zu verwenden, anstatt eine generische Exception
ou Throwable
aber wenn Sie jemals auf diesen irreführenden Kommentar zur Exception stoßen, wissen Sie jetzt, warum die PHPUnit-Exception TypeError
oder andere Ausnahme Fehler sind in der Tat nicht Exception
s aber Throwable
Wenn Sie mit PHP 5.5+ arbeiten, können Sie ::class
Entschließung um den Namen der Klasse mit expectException
/ setExpectedException
. Dies bietet mehrere Vorteile:
- Der Name wird mit seinem Namespace (falls vorhanden) vollqualifiziert.
- Sie löst sich auf in eine
string
so dass es mit jeder Version von PHPUnit funktioniert. - Sie erhalten Code-Vervollständigung in Ihrer IDE.
- Der PHP-Compiler wird einen Fehler ausgeben, wenn Sie den Klassennamen falsch eingeben.
Beispiel:
namespace \My\Cool\Package;
class AuthTest extends \PHPUnit_Framework_TestCase
{
public function testLoginFailsForWrongPassword()
{
$this->expectException(WrongPasswordException::class);
Auth::login('Bob', 'wrong');
}
}
PHP kompiliert
WrongPasswordException::class
in
"\My\Cool\Package\WrongPasswordException"
ohne dass PHPUnit etwas davon mitbekommt.
Hinweis : PHPUnit 5.2 eingeführt
expectException
als Ersatz fürsetExpectedException
.
- See previous answers
- Weitere Antworten anzeigen