PHPUnit断言抛出了异常?

Fel*_*ida 302 php phpunit assert exception

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

Fra*_*mer 507

<?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);
    }
}
Run Code Online (Sandbox Code Playgroud)

expectException()PHPUnit文档

PHPUnit作者文章提供了有关测试异常最佳实践的详细说明.

  • +1不依赖于魔术doc-block (88认同)
  • 在文档或这里没有提到的,但是期望抛出异常的代码需要在**`expectException()之后被调用**.虽然对某些人来说可能是显而易见的,但对我来说这是一个*问题. (36认同)
  • 仅供参考:从[phpunit 5.2.0]开始(https://github.com/sebastianbergmann/phpunit/wiki/Release-Announcement-for-PHPUnit-5.2.0)不推荐使用`setExpectedException`方法,替换为`expectException`一.:) (18认同)
  • 事实上,你无法指定预期会抛出的精确代码行,这是一个错误的IMO.并且无法在同一测试中测试多个异常,这使得对许多预期异常的测试变得非常笨拙.我写了[实际断言](https://github.com/sebastianbergmann/phpunit/issues/1798#issuecomment-134219493)来试图解决这些问题. (14认同)
  • 如果您使用名称空间,那么您需要输入完整的名称空间:`$ this-> setExpectedException('\ My\Name\Space\MyCustomException');` (7认同)
  • 从doc开始并不明显,但是在执行抛出异常的函数之后没有代码会被执行.因此,如果您想在同一测试用例中测试多个异常,则不能. (5认同)
  • 请注意,似乎没有办法明确指出没有异常.您只需调用您的方法,如果抛出异常,测试将自动失败. (2认同)

Dav*_*ess 119

您还可以使用docblock注释:

class ExceptionTest extends PHPUnit_Framework_TestCase
{
    /**
     * @expectedException InvalidArgumentException
     */
    public function testException()
    {
        ...
    }
}
Run Code Online (Sandbox Code Playgroud)

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

  • @LeviMorrison - 恕我直言,异常*消息*不应该被测试,类似于日志消息.在执行*manual*取证时,两者都被认为是无关的,有用的信息.测试的关键是*type*of exception.除此之外的任何事情都与实施过于紧密.`IncorrectPasswordException`应该足够了 - 消息等于`"bob@me.com的密码错误"`是辅助的.除此之外,您希望花费尽可能少的时间编写测试,并开始了解简单测试的重要性. (12认同)
  • @DavidHarkness我认为有人会提起这件事.同样地,我同意测试消息通常过于严格和严格.然而,严格和严密的约束*可能*(有目的地强调)在某些情况下是想要的,例如规范的执行. (5认同)
  • IMO,这是首选方法. (3认同)
  • @MikePurcell,为什么? (3认同)
  • 我不会在文档块中观看以了解它的预期,但我会查看实际的测试代码(无论测试类型如何)。这是所有其他测试的标准;我没有看到 _Exceptions_ 成为(天哪)这个约定的例外的正当理由。 (2认同)
  • “不测试消息”规则听起来是有效的,除非您测试的方法会在代码的多个部分中引发相同的异常类型,唯一的区别是在消息中传递的错误ID。您的系统可能会根据Exception消息(不是Exception类型)向用户显示一条消息。在这种情况下,用户看到的消息并不重要,因此,您应该测试错误消息。 (2认同)

Jim*_*mix 42

太长了;滚动到:使用 PHPUnit 的数据提供程序

\n

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

\n
$this->expectException(string $exceptionClassName);\n$this->expectExceptionCode(int|string $code);\n$this->expectExceptionMessage(string $message);\n$this->expectExceptionMessageMatches(string $regularExpression);\n$this->expectExceptionObject(\\Exception $exceptionObject);\n
Run Code Online (Sandbox Code Playgroud)\n

然而,文档对于测试代码中上述任何方法的顺序含糊其辞。

\n

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

\n
<?php\n\nclass SimpleAssertionTest extends \\PHPUnit\\Framework\\TestCase\n{\n    public function testSimpleAssertion(): void\n    {\n        $expected = \'bar\';\n        $actual = \'bar\';\n        $this->assertSame($expected, $actual);\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

输出:

\n
 \xe2\x9c\x94 Simple assertion\nOK (1 test, 1 assertion)\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n
<?php\n\nuse PHPUnit\\Framework\\TestCase;\n\nfinal class ExceptionTest extends TestCase\n{\n    public function testException(): void\n    {\n        throw new \\InvalidArgumentException();\n        $this->expectException(\\InvalidArgumentException::class);\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

输出:

\n
 \xe2\x9c\x98 Exception\n   \xe2\x94\x9c InvalidArgumentException:\n\nERRORS!\nTests: 1, Assertions: 0, Errors: 1.\n
Run Code Online (Sandbox Code Playgroud)\n

错误是因为:

\n
\n

一旦引发异常,PHP 就无法返回到引发异常的行之后的代码行。捕获异常在这方面没有任何改变。抛出异常是一张单程票。

\n
\n

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

\n

因此 PHPUnit 甚至没有到达这个地方:

\n
 \xe2\x9c\x98 Exception\n   \xe2\x94\x9c InvalidArgumentException:\n\nERRORS!\nTests: 1, Assertions: 0, Errors: 1.\n
Run Code Online (Sandbox Code Playgroud)\n

如果它之前是:

\n
$this->expectException(\\InvalidArgumentException::class);\n
Run Code Online (Sandbox Code Playgroud)\n

而且,无论 PHPUnit 有什么异常捕获能力,它都永远无法到达那个地方。

\n

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

\n
throw new \\InvalidArgumentException();\n
Run Code Online (Sandbox Code Playgroud)\n

必须位于预期抛出异常的代码之前,这与设置实际值之后放置的断言相反。

\n

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

\n
$this->expectException(string $exceptionClassName);\n$this->expectExceptionCode(int|string $code);\n$this->expectExceptionMessage(string $message);\n$this->expectExceptionMessageMatches(string $regularExpression);\n$this->expectExceptionObject(\\Exception $exceptionObject);\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n

已经知道:

\n
\n

一旦引发异常,PHP 就无法返回到引发异常的行之后的代码行。

\n
\n

您应该能够轻松地在此测试中发现错误:

\n
<?php\n\nuse PHPUnit\\Framework\\TestCase;\n\nfinal class ExceptionTest extends TestCase\n{\n    public function testException(): void\n    {\n        $this->expectException(\\InvalidArgumentException::class);\n        throw new \\InvalidArgumentException();\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

第一个$this->expectException()应该没问题,它在抛出预期的确切异常类之前需要一个异常类,所以这里没有问题。

\n

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

\n

测试的输出是:

\n
<?php\nnamespace VendorName\\PackageName;\n\nclass ExceptionTest extends \\PHPUnit\\Framework\\TestCase\n{\n    public function testThrowException(): void\n    {\n        # Should be OK\n        $this->expectException(\\RuntimeException::class);\n        throw new \\RuntimeException();\n\n        # Should Fail\n        $this->expectException(\\RuntimeException::class);\n        throw new \\InvalidArgumentException();\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

OK

\n

OK不,测试距离通过还很远,并且应该Fail在第二个例外情况下通过。这是为什么?

\n

请注意,输出有:

\n
\n

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

\n
\n

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

\n

应该有 2 个断言 = OKFail这使得测试无法通过。

\n

这只是因为 PHPUnittestThrowException在以下行之后执行完毕:

\n
 \xe2\x9c\x94 Throw exception\n\nOK (1 test, 1 assertion)\n
Run Code Online (Sandbox Code Playgroud)\n

这是一张单程票,超出testThrowException了 PHPUnit 捕获\\RuntimeException并执行其需要执行的操作的范围,但无论它能做什么,我们都知道它将无法跳回到testThrowException代码中:

\n
throw new \\RuntimeException();\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n

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

\n
# Should Fail\n$this->expectException(\\RuntimeException::class);\nthrow new \\InvalidArgumentException();\n
Run Code Online (Sandbox Code Playgroud)\n

给出错误:

\n
\n

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

\n
\n

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

\n

如何测试多个异常?

\n

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

\n
<?php\nnamespace VendorName\\PackageName;\n\nclass ExceptionTest extends \\PHPUnit\\Framework\\TestCase\n{\n    public function testThrowException(): void\n    {\n        # OK\n        $this->expectException(\\RuntimeException::class);\n        throw new \\RuntimeException(\'Something went wrong\');\n\n        # Fail\n        $this->expectExceptionMessage(\'This code will never be executed\');\n        throw new \\RuntimeException(\'Something went wrong\');\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

给出:

\n
<?php\nnamespace VendorName\\PackageName;\n\nclass ExceptionTest extends \\PHPUnit\\Framework\\TestCase\n{\n    public function testThrowExceptionBar(): void\n    {\n        # OK\n        $this->expectException(\\RuntimeException::class);\n        throw new \\RuntimeException();\n    }\n\n    public function testThrowExceptionFoo(): void\n    {\n        # Fail\n        $this->expectException(\\RuntimeException::class);\n        throw new \\InvalidArgumentException();\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

FAILURES正如它应该。

\n

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

\n

捕获异常并用断言检查它

\n

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

\n
 \xe2\x9c\x94 Throw exception bar\n \xe2\x9c\x98 Throw exception foo\n   \xe2\x94\x90\n   \xe2\x94\x9c Failed asserting that exception of type "InvalidArgumentException" matches expected exception "RuntimeException". Message was: "" at\n\nFAILURES!\nTests: 2, Assertions: 2, Failures: 1.\n
Run Code Online (Sandbox Code Playgroud)\n

给出:

\n
 \xe2\x9c\x98 Throw exception\n   \xe2\x94\x90\n   \xe2\x94\x9c Failed asserting that two strings are identical.\n   \xe2\x94\x8a ---\xc2\xb7Expected\n   \xe2\x94\x8a +++\xc2\xb7Actual\n   \xe2\x94\x8a @@ @@\n   \xe2\x94\x8a -\'Something\xc2\xb7went\xc2\xb7wrong\'\n   \xe2\x94\x8a +\'I\xc2\xb7MUST\xc2\xb7FAIL\xc2\xb7!\'\n\nFAILURES!\nTests: 1, Assertions: 5, Failures: 1.\n
Run Code Online (Sandbox Code Playgroud)\n

FAILURES应该如此,但是天哪,您读过上面的内容了吗?您需要注意清除变量unset($className);以检测是否引发了异常,然后该生物$location = __FILE__ ...拥有异常的精确位置(以防未引发异常),然后检查是否引发了异常if (empty($className)) { ... }并使用$this->fail($failMsg);并在未引发异常时信号发出信号抛出。

\n

使用 PHPUnit 的数据提供程序

\n

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

\n

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

\n
\n

一旦引发异常,PHP 就无法返回到引发异常的行之后的代码行。

\n
\n

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

\n

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

\n

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

\n
<?php\nnamespace VendorName\\PackageName;\n\nclass ExceptionTest extends \\PHPUnit\\Framework\\TestCase\n{\n    public function testThrowException(): void\n    {\n        # OK\n        unset($className);\n        try {\n            $location = __FILE__ . \':\' . (string) (__LINE__ + 1);\n            throw new \\RuntimeException(\'Something went wrong\'); \n\n        } catch (\\Exception $e) {\n            $className = get_class($e);\n            $msg = $e->getMessage();\n            $code = $e->getCode();\n        }\n\n        $expectedClass = \\RuntimeException::class;\n        $expectedMsg = \'Something went wrong\';\n        $expectedCode = 0;\n\n        if (empty($className)) {\n            $failMsg = \'Exception: \' . $expectedClass;\n            $failMsg .= \' with msg: \' . $expectedMsg;\n            $failMsg .= \' and code: \' . $expectedCode;\n            $failMsg .= \' at: \' . $location;\n            $failMsg .= \' Not Thrown!\';\n            $this->fail($failMsg);\n        }\n\n        $this->assertSame($expectedClass, $className);\n        $this->assertSame($expectedMsg, $msg);\n        $this->assertSame($expectedCode, $code);\n\n        # ------------------------------------------\n\n        # Fail\n        unset($className);\n        try {\n            $location = __FILE__ . \':\' . (string) (__LINE__ + 1);\n            throw new \\InvalidArgumentException(\'I MUST FAIL !\'); \n\n        } catch (\\Exception $e) {\n            $className = get_class($e);\n            $msg = $e->getMessage();\n            $code = $e->getCode();\n        }\n\n        $expectedClass = \\InvalidArgumentException::class;\n        $expectedMsg = \'Something went wrong\';\n        $expectedCode = 0;\n\n        if (empty($className)) {\n            $failMsg = \'Exception: \' . $expectedClass;\n            $failMsg .= \' with msg: \' . $expectedMsg;\n            $failMsg .= \' and code: \' . $expectedCode;\n            $failMsg .= \' at: \' . $location;\n            $failMsg .= \' Not Thrown!\';\n            $this->fail($failMsg);\n        }\n\n        $this->assertSame($expectedClass, $className);\n        $this->assertSame($expectedMsg, $msg);\n        $this->assertSame($expectedCode, $code);\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

给出结果:

\n
 \xe2\x9c\x98 Throw exception\n   \xe2\x94\x90\n   \xe2\x94\x9c Failed asserting that two strings are identical.\n   \xe2\x94\x8a ---\xc2\xb7Expected\n   \xe2\x94\x8a +++\xc2\xb7Actual\n   \xe2\x94\x8a @@ @@\n   \xe2\x94\x8a -\'Something\xc2\xb7went\xc2\xb7wrong\'\n   \xe2\x94\x8a +\'I\xc2\xb7MUST\xc2\xb7FAIL\xc2\xb7!\'\n\nFAILURES!\nTests: 1, Assertions: 5, Failures: 1.\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n
\n

好的(2 个测试,2 个断言)

\n
\n

所以即使是这一行:

\n
<?php\n\nclass ExceptionCheck\n{\n    public function throwE($data)\n    {\n        if ($data === 1) {\n            throw new \\RuntimeException;\n        } else {\n            throw new \\InvalidArgumentException;\n        }\n    }\n}\n\nclass ExceptionTest extends \\PHPUnit\\Framework\\TestCase\n{\n    public function ExceptionTestProvider() : array\n    {\n        $data = [\n            \\RuntimeException::class =>\n            [\n                [\n                    \'input\' => 1,\n                    \'className\' => \\RuntimeException::class\n                ]\n            ],\n\n            \\InvalidArgumentException::class =>\n            [\n                [\n                    \'input\' => 2,\n                    \'className\' => \\InvalidArgumentException::class\n                ]\n            ]\n        ];\n        return $data;\n    }\n\n    /**\n     * @dataProvider ExceptionTestProvider\n     */\n    public function testThrowException($data): void\n    {\n        $this->expectException($data[\'className\']);\n        $exceptionCheck = new ExceptionCheck;\n\n        $exceptionCheck->throwE($data[\'input\']);\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n

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

\n

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

\n
\n

\xe2\x9c\x94 使用RuntimeException抛出异常

\n

\xe2\x9c\x94 使用InvalidArgumentException抛出异常

\n
\n

如果你改变线路:

\n
 \xe2\x9c\x94 Throw exception with RuntimeException\n \xe2\x9c\x94 Throw exception with InvalidArgumentException\n\nOK (2 tests, 2 assertions)\n
Run Code Online (Sandbox Code Playgroud)\n

到:

\n
if ($data !== 1) {\n
Run Code Online (Sandbox Code Playgroud)\n

public function throwE($data)

\n

要抛出错误的异常并再次运行 PHPUnit,您将看到:

\n
 \xe2\x9c\x98 Throw exception with RuntimeException\n   \xe2\x94\x9c Failed asserting that exception of type "InvalidArgumentException" matches expected exception "RuntimeException". Message was: "" at (...)\n\n \xe2\x9c\x98 Throw exception with InvalidArgumentException\n   \xe2\x94\x9c Failed asserting that exception of type "RuntimeException" matches expected exception "InvalidArgumentException". Message was: "" at (...)\n\nFAILURES!\nTests: 2, Assertions: 2, Failures: 2.\n
Run Code Online (Sandbox Code Playgroud)\n

正如预期:

\n
\n

失败!\n测试:2,断言:2,失败:2。

\n
\n

准确指出了导致一些问题的数据集名称:

\n
\n

\xe2\x9c\x98 使用RuntimeException抛出异常

\n

\xe2\x9c\x98 使用InvalidArgumentException抛出异常

\n
\n

制作 public function throwE($data)抛出任何异常:

\n
$exceptionCheck->throwE($data[\'input\']);\n
Run Code Online (Sandbox Code Playgroud)\n

再次运行 PHPUnit 给出:

\n
if ($data === 1) {\n
Run Code Online (Sandbox Code Playgroud)\n

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

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

测试异常陷阱

\n

“TypeError”类型的异常

\n

PHP7 数据类型支持此测试:

\n
if ($data !== 1) {\n
Run Code Online (Sandbox Code Playgroud)\n

失败并输出:

\n
 \xe2\x9c\x98 Say\n   \xe2\x94\x9c 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 (..)\n\nFAILURES!\nTests: 1, Assertions: 1, Failures: 1.\n
Run Code Online (Sandbox Code Playgroud)\n

即使方法中有say

\n
 \xe2\x9c\x98 Throw exception with RuntimeException\n   \xe2\x94\x9c Failed asserting that exception of type "InvalidArgumentException" matches expected exception "RuntimeException". Message was: "" at (...)\n\n \xe2\x9c\x98 Throw exception with InvalidArgumentException\n   \xe2\x94\x9c Failed asserting that exception of type "RuntimeException" matches expected exception "InvalidArgumentException". Message was: "" at (...)\n\nFAILURES!\nTests: 2, Assertions: 2, Failures: 2.\n
Run Code Online (Sandbox Code Playgroud)\n

并且测试传递了一个数组而不是一个字符串:

\n
public function throwE($data)\n{\n}\n
Run Code Online (Sandbox Code Playgroud)\n

PHP 未到达代码:

\n
 \xe2\x9c\x98 Throw exception with RuntimeException\n   \xe2\x94\x9c Failed asserting that exception of type "RuntimeException" is thrown.\n\n \xe2\x9c\x98 Throw exception with InvalidArgumentException\n   \xe2\x94\x9c Failed asserting that exception of type "InvalidArgumentException" is thrown.\n\nFAILURES!\nTests: 2, Assertions: 2, Failures: 2.\n
Run Code Online (Sandbox Code Playgroud)\n

因为由于类型输入而提前引发了异常string

\n
<?php\ndeclare(strict_types=1);\n\nclass DatatypeChat\n{\n    public function say(string $msg)\n    {\n        if (!is_string($msg)) {\n            throw new \\InvalidArgumentException(\'Message must be a string\');\n        }\n        return "Hello $msg";\n    }\n}\n\nclass ExceptionTest extends \\PHPUnit\\Framework\\TestCase\n{\n    public function testSay(): void\n    {\n        $this->expectException(\\InvalidArgumentException::class);\n        $chat = new DatatypeChat;\n        $chat->say(array());\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

因此 被 TypeError抛出而不是InvalidArgumentException

\n

再次出现“TypeError”类型异常

\n

知道我们不需要if (!is_string($msg))检查数据类型,因为 PHP 已经注意到,如果我们在方法声明中指定数据类型,如果消息太长,say(string $msg)我们可能想要抛出 异常。InvalidArgumentExceptionif (strlen($msg) > 3)

\n
 \xe2\x9c\x98 Say\n   \xe2\x94\x9c 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 (..)\n\nFAILURES!\nTests: 1, Assertions: 1, Failures: 1.\n
Run Code Online (Sandbox Code Playgroud)\n

还进行修改,ExceptionTest因此我们有两种情况(测试方法)Exception应该抛出异常 - 第一种testSayTooLong是当消息太长时,第二种testSayDataType是当消息类型错误时。

\n

在这两个测试中,我们期望使用以下方法来代替特定的异常类(例如InvalidArgumentException通用TypeError类)Exception

\n

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

\n

测试结果是:

\n
if (!is_string($msg)) {\n   throw new \\InvalidArgumentException(\'Message must be a string\');\n}\n
Run Code Online (Sandbox Code Playgroud)\n

testSayTooLong()期待通用Exception并使用

\n

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

\n

OK当 被InvalidArgumentException抛出时通过

\n

\n

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

\n
\n

无法断言“ TypeError ”类型的异常与预期的异常“ Exception ”匹配。

\n
\n

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

\n

问题在于 PHPUnit 的上述描述会误导您,因为 PHPUnit并不TypeError例外。不延伸自TypeErrorException或其任何其他子类延伸。

\n

TypeError实现Throwable 接口见文档

\n

然而

\n

InvalidArgumentException扩展LogicException 文档

\n

LogicException扩展Exception 文档

\n

从而也InvalidArgumentException延伸。Exception

\n

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

\n

  • 这是我见过的最大的答案 (14认同)
  • 不确定我是否应该标记为“将整个博客文章粘贴为答案” (2认同)

Dav*_*ess 34

如果您在PHP 5.5+上运行,则可以使用::class分辨率来获取带有expectException/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');
    }
}
Run Code Online (Sandbox Code Playgroud)

PHP编译

WrongPasswordException::class
Run Code Online (Sandbox Code Playgroud)

"\My\Cool\Package\WrongPasswordException"
Run Code Online (Sandbox Code Playgroud)

没有PHPUnit是更明智的.

注意:PHPUnit 5.2 expectException作为替代品引入setExpectedException.


Far*_*mov 29

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

重要提示:如果没有抛出预期的异常,它将失败.

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());
}
Run Code Online (Sandbox Code Playgroud)

  • `$ this-> fail()`不是以这种方式使用我不认为,至少目前不是(PHPUnit 3.6.11); 它本身就是一个例外.使用你的例子,如果调用`$ this->失败("未抛出预期的异常")`,则触发`catch`块并且`$ e-> getMessage()`是_"未抛出预期的异常"_ . (6认同)
  • 我认为这种方法不能很好地工作的原因是它使用`catch(Exception $ e)`来捕获所有异常.当我尝试捕获特定的异常时,这种方法对我很有效:`try {throw new MySpecificException; $ this-> fail('MySpecificException not thrown'); } catch(MySpecificException $ e){}` (5认同)
  • 我必须投反对票,因为对 `fail` 的调用不应该在 `try` 块中。它本身会触发 `catch` 块产生错误的结果。 (2认同)

hej*_*dav 23

您可以使用assertException扩展在一次测试执行期间声明多个异常.

将方法插入TestCase并使用:

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

我也为漂亮的代码爱好者做了一个特质.

  • `asertException`方法不是原始PHPUnit的一部分.您必须手动继承`PHPUnit_Framework_TestCase`类并添加[在上面的帖子中链接的方法](https://gist.github.com/VladaHejda/8826707).然后,您的测试用例将继承此继承的类. (2认同)

小智 14

另一种方法可以是:

$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Expected Exception Message');
Run Code Online (Sandbox Code Playgroud)

请确保您的测试类范围\PHPUnit_Framework_TestCase.

  • 看起来 `expectExceptionMessage` 的行为就像一个正则表达式。如果您的错误消息是“Foo bar Baz”,则“$this-&gt;expectExceptionMessage('Foo');”将使测试通过。 (2认同)
  • 就是这个!另外,“expectExceptionCode(401)” (2认同)

Fin*_*sse 9

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

我已经使这个辅助函数断言某些函数抛出异常:

/**
 * 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');
}
Run Code Online (Sandbox Code Playgroud)

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

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


小智 8

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

}
Run Code Online (Sandbox Code Playgroud)


jch*_*ook 7

综合解决方案

PHPUnit目前用于异常测试的" 最佳实践 "似乎......乏味(docs).

由于我强烈反对当前的expectException实现,我在我的测试用例中使用了一个特性.它只有50行.

  • 每个测试支持多个异常
  • 支持抛出异常后调用的断言
  • 强大而清晰的用法示例
  • 标准assert语法
  • 支持断言不仅仅是消息,代码和类
  • 支持反向断言, assertNotThrows

图书馆

我将这个Throwable特性发布给了Github和packagist,所以它可以和作曲家一起安装.

简单的例子

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

<?php

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

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

很简约?


完整用法示例

请参阅下面的更全面的用法示例:

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

?>
Run Code Online (Sandbox Code Playgroud)

  • 具有讽刺意味的是,您用于单元测试的软件包在回购中不包含单元测试。 (2认同)
  • @domdambrogia 感谢 [@jean-beguin](https://github.com/jchook/phpunit-assert-throws/commit/6c0666a7569211c7bfca5b67463cdfe7cdf42cc4#diff-b0d7da0224e1e9033983cf3cf3cf7e903983cb3e (2认同)

Wes*_*y92 6

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

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!');
    }
}
Run Code Online (Sandbox Code Playgroud)

文档可以在这里找到。