PHPUnit断言一个异常被抛出?

有人知道是否有assert或类似的东西可以测试是否在被测试的代码中抛出异常吗?

311585 次浏览
<?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文档 .

PHPUnit作者文章提供了关于测试异常最佳实践的详细解释。

你也可以在PHPUnit 9发布之前使用docblock注释:

class ExceptionTest extends PHPUnit_Framework_TestCase
{
/**
* @expectedException InvalidArgumentException
*/
public function testException()
{
...
}
}

对于PHP 5.5+(特别是带有命名空间的代码),我现在更喜欢使用::class

下面的代码将测试异常消息和异常代码。

重要的是:如果没有抛出预期的异常,则会失败。

try{
$test->methodWhichWillThrowException();//if this method not throw exception it must be fail too.
$this->fail("Expected exception 1162011 not thrown");
}catch(MySpecificException $e){ //Not catching a generic Exception or the fail function is also catched
$this->assertEquals(1162011, $e->getCode());
$this->assertEquals("Exception Message", $e->getMessage());
}

在一次测试执行期间,可以使用assertException扩展断言多个异常。

插入方法到您的TestCase并使用:

public function testSomething()
{
$test = function() {
// some code that has to throw an exception
};
$this->assertException( $test, 'InvalidArgumentException', 100, 'expected message' );
}

我还做了一个trait的好代码爱好者..

public function testException() {
try {
$this->methodThatThrowsException();
$this->fail("Expected Exception has not been raised.");
} catch (Exception $ex) {
$this->assertEquals("Exception message", $ex->getMessage());
}
    

}

如果你在PHP 5.5+上运行,你可以使用::class决议来获得带有__ABC1 / setExpectedException的类名。这有几个好处:

  • 名称将完全限定其名称空间(如果有的话)。
  • 它解析为string,因此它可以与任何版本的PHPUnit一起工作。
  • 在IDE中实现代码完成。
  • 如果键入错误的类名,PHP编译器将发出一个错误。

例子:

namespace \My\Cool\Package;


class AuthTest extends \PHPUnit_Framework_TestCase
{
public function testLoginFailsForWrongPassword()
{
$this->expectException(WrongPasswordException::class);
Auth::login('Bob', 'wrong');
}
}

PHP编译

WrongPasswordException::class

"\My\Cool\Package\WrongPasswordException"

没有PHPUnit是明智的。

请注意: PHPUnit 5.2的介绍 expectException作为setExpectedException的替换。

/**
* @expectedException Exception
* @expectedExceptionMessage Amount has to be bigger then 0!
*/
public function testDepositNegative()
{
$this->account->deposit(-7);
}
"/**"要非常小心,注意双“*”。只写“**”(asterix)将使代码失败。 同时确保你使用的是phpUnit的最新版本。在phpunit的一些早期版本中,不支持@expectedException异常。我有4.0,但它不适合我,我必须更新到5.5 https://coderwall.com/p/mklvdw/install-phpunit-with-composer才能更新composer

另一种方法是:

$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Expected Exception Message');

请确保您的测试类扩展\PHPUnit_Framework_TestCase

以下是您可以执行的所有异常断言。注意它们都是可选

class ExceptionTest extends PHPUnit_Framework_TestCase
{
public function testException()
{
// make your exception assertions
$this->expectException(InvalidArgumentException::class);
// if you use namespaces:
// $this->expectException('\Namespace\MyExceptio‌​n');
$this->expectExceptionMessage('message');
$this->expectExceptionMessageRegExp('/essage$/');
$this->expectExceptionCode(123);
// code that throws an exception
throw new InvalidArgumentException('message', 123);
}


public function testAnotherException()
{
// repeat as needed
$this->expectException(Exception::class);
throw new Exception('Oh no!');
}
}

文档可以在在这里中找到。

PHPUnit expectException方法非常不方便,因为它只允许每个测试方法测试一个异常。

我使用这个helper函数来断言某个函数抛出异常:

/**
* Asserts that the given callback throws the given exception.
*
* @param string $expectClass The name of the expected exception class
* @param callable $callback A callback which should throw the exception
*/
protected function assertException(string $expectClass, callable $callback)
{
try {
$callback();
} catch (\Throwable $exception) {
$this->assertInstanceOf($expectClass, $exception, 'An invalid exception was thrown');
return;
}


$this->fail('No exception was thrown');
}

将它添加到您的测试类中,并以这样的方式调用:

public function testSomething() {
$this->assertException(\PDOException::class, function() {
new \PDO('bad:param');
});
$this->assertException(\PDOException::class, function() {
new \PDO('foo:bar');
});
}

全面的解决方案

PHPUnit当前的“最佳实践”异常测试似乎..低迷(文档)。

由于我想要更多比当前的expectException实现,我做了一个trait在我的测试用例上使用。它只是大约50行代码

  • 每个测试支持多个异常
  • 支持异常抛出后调用的断言
  • 健壮和清晰的使用示例
  • 标准assert语法
  • 不仅支持消息、代码和类的断言
  • 支持逆断言,assertNotThrows
  • 支持PHP 7 Throwable错误

图书馆

我将AssertThrows特性发布到Github和packagist,这样它就可以与composer一起安装。

简单的例子

只是为了说明语法背后的精神:

<?php


// Using simple callback
$this->assertThrows(MyException::class, [$obj, 'doSomethingBad']);


// Using anonymous function
$this->assertThrows(MyException::class, function() use ($obj) {
$obj->doSomethingBad();
});

很整洁的?


完整使用示例

请看下面一个更全面的使用示例:

<?php


declare(strict_types=1);


use Jchook\AssertThrows\AssertThrows;
use PHPUnit\Framework\TestCase;


// These are just for illustration
use MyNamespace\MyException;
use MyNamespace\MyObject;


final class MyTest extends TestCase
{
use AssertThrows; // <--- adds the assertThrows method


public function testMyObject()
{
$obj = new MyObject();


// Test a basic exception is thrown
$this->assertThrows(MyException::class, function() use ($obj) {
$obj->doSomethingBad();
});


// Test custom aspects of a custom extension class
$this->assertThrows(MyException::class,
function() use ($obj) {
$obj->doSomethingBad();
},
function($exception) {
$this->assertEquals('Expected value', $exception->getCustomThing());
$this->assertEquals(123, $exception->getCode());
}
);


// Test that a specific exception is NOT thrown
$this->assertNotThrows(MyException::class, function() use ($obj) {
$obj->doSomethingGood();
});
}
}


?>

对于PHPUnit 5.7.27和PHP 5.6,要在一个测试中测试多个异常,强制进行异常测试是很重要的。如果没有异常发生,则单独使用异常处理来断言exception实例将跳过对情况的测试。

public function testSomeFunction() {


$e=null;
$targetClassObj= new TargetClass();
try {
$targetClassObj->doSomething();
} catch ( \Exception $e ) {
}
$this->assertInstanceOf(\Exception::class,$e);
$this->assertEquals('Some message',$e->getMessage());


$e=null;
try {
$targetClassObj->doSomethingElse();
} catch ( Exception $e ) {
}
$this->assertInstanceOf(\Exception::class,$e);
$this->assertEquals('Another message',$e->getMessage());


}
function yourfunction($a,$z){
if($a<$z){ throw new <YOUR_EXCEPTION>; }
}

下面是测试

class FunctionTest extends \PHPUnit_Framework_TestCase{


public function testException(){


$this->setExpectedException(<YOUR_EXCEPTION>::class);
yourfunction(1,2);//add vars that cause the exception


}


}

PhpUnit是一个很棒的库,但这一点有点令人沮丧。这就是为什么我们可以使用turbotesting-php开源库,它有一个非常方便的断言方法来帮助我们测试异常。在这里可以找到:

https://github.com/edertone/TurboTesting/blob/master/TurboTesting-Php/src/main/php/utils/AssertUtils.php

要使用它,我们只需执行以下操作:

AssertUtils::throwsException(function(){


// Some code that must throw an exception here


}, '/expected error message/');

如果我们在匿名函数中键入的代码没有抛出异常,则会抛出异常。

如果我们在匿名函数中键入的代码抛出异常,但其消息与预期的regexp不匹配,则也将抛出异常。

TLDR;使用PHPUnit的数据提供程序

PHPUnit 9.5提供了以下方法来测试异常:

$this->expectException(string $exceptionClassName);
$this->expectExceptionCode(int|string $code);
$this->expectExceptionMessage(string $message);
$this->expectExceptionMessageMatches(string $regularExpression);
$this->expectExceptionObject(\Exception $exceptionObject);

然而,文档对于测试代码中上述任何方法的顺序都是模糊的。

如果你习惯使用断言,例如:

<?php


class SimpleAssertionTest extends \PHPUnit\Framework\TestCase
{
public function testSimpleAssertion(): void
{
$expected = 'bar';
$actual = 'bar';
$this->assertSame($expected, $actual);
}
}

输出:

 ✔ Simple assertion
OK (1 test, 1 assertion)

你可能会对异常测试失败感到惊讶:

<?php


use PHPUnit\Framework\TestCase;


final class ExceptionTest extends TestCase
{
public function testException(): void
{
throw new \InvalidArgumentException();
$this->expectException(\InvalidArgumentException::class);
}
}

输出:

 ✘ Exception
├ InvalidArgumentException:


ERRORS!
Tests: 1, Assertions: 0, Errors: 1.

这个错误是因为:

一旦抛出异常,PHP就不能返回到抛出异常的行之后的代码行。在这方面,捕获异常不会改变什么。抛出异常是一种单程票。

与错误不同,异常不具备从异常中恢复的能力,并使PHP继续代码执行,就像没有异常一样。

因此PHPUnit甚至没有到达:

$this->expectException(\InvalidArgumentException::class);

如果它前面有:

throw new \InvalidArgumentException();

而且,无论PHPUnit的异常捕获能力如何,它都永远无法到达这个位置。

因此,使用PHPUnit的任何异常测试方法:

$this->expectException(string $exceptionClassName);
$this->expectExceptionCode(int|string $code);
$this->expectExceptionMessage(string $message);
$this->expectExceptionMessageMatches(string $regularExpression);
$this->expectExceptionObject(\Exception $exceptionObject);

必须为之前,在此代码中,预计会抛出与放置的实际值相反的异常。

使用异常测试的正确顺序:

<?php


use PHPUnit\Framework\TestCase;


final class ExceptionTest extends TestCase
{
public function testException(): void
{
$this->expectException(\InvalidArgumentException::class);
throw new \InvalidArgumentException();
}
}

由于必须在抛出异常之前调用PHPUnit内部方法来测试异常,因此与测试异常相关的PHPUnit方法从$this->excpect而不是$this->assert开始是有意义的。

已经知道:

一旦抛出异常,PHP就不能返回到抛出异常的行之后的代码行。

你应该能够很容易地在这个测试中发现一个bug:

<?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();
}
}

第一个$this->expectException()应该是OK的,它在抛出预期的确切异常类之前期望一个异常类,所以这里没有错误。

第二个应该失败的是在一个完全不同的异常抛出之前期望RuntimeException类,所以它应该失败,但PHPUnit执行是否会到达那个地方?

测试的输出是:

 ✔ Throw exception


OK (1 test, 1 assertion)

OK吗?

不,如果测试通过,它离OK很远,并且在第二个异常时它应该Fail。为什么呢?

注意输出有:

OK(1个测试,1个断言)

其中测试计数是正确的,但只有1 assertion

应该有两个断言= OKFail使测试不通过。

这只是因为PHPUnit在行后执行了testThrowException:

throw new \RuntimeException();

这是在testThrowException范围之外到PHPUnit捕获\RuntimeException并做它需要做的事情的地方的单程票,但无论它能做什么,我们知道它将无法跳回testThrowException,因此代码如下:

# Should Fail
$this->expectException(\RuntimeException::class);
throw new \InvalidArgumentException();

将永远不会被执行,这就是为什么从PHPUnit的角度来看,测试结果是OK而不是Fail

如果你想在同一个测试方法中使用多个$this->expectException()$this->expectException()$this->expectExceptionMessage()的混合调用,这不是一个好消息:

<?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');
}
}

给错了:

OK(1个测试,1个断言)

因为一旦抛出异常,所有其他与测试异常相关的$this->expect...调用将不会被执行,PHPUnit测试用例结果将只包含第一个预期异常的结果。

如何测试多个异常?

将多个异常拆分到单独的测试中:

<?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();
}
}

给:

 ✔ Throw exception bar
✘ Throw exception foo
┐
├ Failed asserting that exception of type "InvalidArgumentException" matches expected exception "RuntimeException". Message was: "" at


FAILURES!
Tests: 2, Assertions: 2, Failures: 1.

FAILURES,因为它应该。

然而,这种方法在其基本方法上有一个缺点——对于每个抛出的异常,您都需要单独的测试。这将产生大量的测试,只是为了检查异常。

捕获异常并使用断言检查它

如果在抛出异常后你不能继续执行脚本,你可以简单地捕获一个预期异常,然后用异常提供的方法获得关于它的所有数据,并将其与预期值和断言结合使用:

<?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);
}
}

给:

 ✘ 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,因为它应该,但哦,我的上帝,你读了上面所有的吗?你需要注意清除变量unset($className);来检测异常是否被抛出,然后这个生物$location = __FILE__ ...来获得异常的精确位置,以防它没有被抛出,然后检查异常是否被抛出if (empty($className)) { ... },并使用$this->fail($failMsg);来发出信号,如果异常没有被抛出。

使用PHPUnit的数据提供程序

PHPUnit有一个叫做数据提供商的有用机制。数据提供程序是返回带有数据集的数据(数组)的方法。当PHPUnit调用测试方法testThrowException时,使用单个数据集作为参数。

如果数据提供程序返回多个数据集,则测试方法将运行多次,每次返回另一个数据集。当测试多个异常或/和多个异常的属性(如类名、消息、代码)时,这是很有帮助的,因为即使:

一旦抛出异常,PHP就不能返回到抛出异常的行之后的代码行。

PHPUnit将多次运行测试方法,每次使用不同的数据集,这样就不会在单个测试方法中运行多个异常(这会失败)。

这就是为什么我们可以让一个测试方法一次只负责测试一个异常,但通过使用PHPUnit的数据提供程序,使用不同的输入数据和预期异常多次运行该测试方法。

数据提供程序方法的定义可以通过对应该由数据提供程序提供的数据集的测试方法进行@dataProvider注释来完成。

<?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']);
}
}

给出结果:

 ✔ Throw exception with RuntimeException
✔ Throw exception with InvalidArgumentException


OK (2 tests, 2 assertions)

注意,即使在整个ExceptionTest中只有一个测试方法,PHPUnit的输出也是:

OK (2测试,2断言)

所以即使是这一行

$exceptionCheck->throwE($data['input']);

在第一次抛出异常时,使用相同的测试方法测试另一个异常是没有问题的,因为由于数据提供程序,PHPUnit使用不同的数据集再次运行它。

数据提供程序返回的每个数据集都可以命名,您只需要使用一个字符串作为存储数据集的键。因此,预期的异常类名被使用了两次。作为数据集数组的键和值(在'className'键下),稍后用作$this->expectException()的参数。

使用字符串作为数据集的键名可以做出漂亮且不言自明的总结:

RuntimeException抛出异常

InvalidArgumentException抛出异常

如果你改变这一行:

if ($data === 1) {

:

if ($data !== 1) {

public function throwE($data)

抛出错误的异常,再次运行PHPUnit,你会看到:

 ✘ Throw exception with RuntimeException
├ Failed 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.

像预期的那样:

< p >失败! 测试:2,断言:2,失败:2.

准确地指出了造成一些问题的数据集名称:

所以用RuntimeException抛出异常

所以用InvalidArgumentException抛出异常

使public function throwE($data)不抛出任何异常:

public function throwE($data)
{
}

再次运行PHPUnit得到:

 ✘ 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.

看起来使用数据提供程序有几个优点:

  1. 输入数据和/或预期数据与实际测试方法分离。
  2. 每个数据集都可以有一个描述性的名称,清楚地指出哪些数据集导致测试通过或失败。
  3. 在测试失败的情况下,您会得到一条正确的失败消息,指出没有抛出异常或抛出了错误的异常,而不是断言x不是y。
  4. 测试可能引发多个异常的单个方法只需要一个测试方法。
  5. 可以测试多个异常和/或多个异常的属性,如类名、消息、代码。
  6. 不需要任何非必要的代码,如try catch块,而只是使用内置的PHPUnit的特性。

测试异常陷阱

类型异常;

与PHP7数据类型支持这个测试:

<?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());
}
}

输出失败:

 ✘ 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.

即使在方法say中存在:

if (!is_string($msg)) {
throw new \InvalidArgumentException('Message must be a string');
}

测试传递的是一个数组而不是字符串:

$chat->say(array());

PHP没有达到代码:

throw new \InvalidArgumentException('Message must be a string');

因为之前由于类型为string而引发异常:

public function say(string $msg)

因此抛出的是TypeError而不是InvalidArgumentException

类型异常;再一次

知道我们不需要if (!is_string($msg))来检查数据类型,因为PHP已经关心了这一点,如果我们在方法声明say(string $msg)中指定了数据类型,我们可能想要抛出InvalidArgumentException,如果消息太长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());
}
}

同时修改ExceptionTest,这样我们就有了两个应该抛出Exception的情况(测试方法)——当消息太长时第一个抛出testSayTooLong,当消息类型错误时第二个抛出testSayDataType

在这两个测试中,我们期望的不是像InvalidArgumentExceptionTypeError这样的特定异常类,而是一个通用的Exception

$this->expectException(\Exception::class);

测试结果为:

 ✔ 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()期望一个泛型Exception,并使用

$this->expectException(\Exception::class);

InvalidArgumentException被抛出时,与OK一起传递

testSayDataType()使用与描述相同的$this->expectException(\Exception::class); Fails:

不能断言类型为“__abc1”的异常;匹配预期异常“__abc2”;

看起来令人困惑的是PHPUnit抱怨异常 TypeError不是Exception,否则它不会在testSayDataType()中对$this->expectException(\Exception::class);有任何问题,因为它对testSayTooLong()抛出InvalidArgumentException并期望:$this->expectException(\Exception::class);没有任何问题

问题是PHPUnit用上面的描述误导了你,因为TypeError异常。TypeError既不能从Exception类扩展,也不能从它的任何其他子类扩展。

TypeError实现了Throwable接口,参见文档

InvalidArgumentException扩展了LogicException 文档

LogicException扩展Exception 文档

因此InvalidArgumentException也扩展了Exception

这就是为什么抛出InvalidArgumentException通过OK和$this->expectException(\Exception::class);的测试,但抛出TypeError却不能(它没有扩展Exception)

然而,ExceptionTypeError都实现了Throwable接口。

因此在两个测试中都有变化

$this->expectException(\Exception::class);

$this->expectException(\Throwable::class);

使测试变为绿色:

 ✔ Say too long
✔ Say data type


OK (2 tests, 2 assertions)

请参阅错误和异常类列表以及它们之间的关系

需要明确的是:在单元测试中使用特定的异常或错误而不是通用的ExceptionThrowable是一个很好的实践,但如果你遇到过关于异常的误导性注释,现在你就会知道为什么PHPUnit的异常TypeError或其他异常错误实际上不是Exception,而是Throwable