PHPUnit如何断言是否抛出了异常?

467

请问是否有一种assert或类似的东西可以测试正在测试的代码中是否抛出了异常?


3
针对这些回答:如果一个测试函数中有多个断言,我只期望其中一个抛出异常,我是否必须将其分开并放入独立的测试函数中? - Panwen Wang
2
@PanwenWang 要测试多个异常或从异常的getter函数中返回多个值,请参见此答案 - Jimmix
15个回答

704
<?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 documentation

PHPUnit author article 提供了关于测试异常最佳实践的详细解释。


13
如果您使用命名空间,那么您需要输入完整的命名空间:$this->setExpectedException('\My\Name\Space\MyCustomException'); - Alcalyn
21
在我看来,无法指定预期抛出异常的确切代码行是一个错误。而且在同一个测试中无法测试多个异常,使得测试许多预期异常变得非常麻烦。我编写了一个实际的断言来尝试解决这些问题。具体内容请见链接:https://github.com/sebastianbergmann/phpunit/issues/1798#issuecomment-134219493 - mindplay.dk
22
FYI:自phpunit 5.2.0起,setExpectedException方法已被弃用,取而代之的是expectException方法。 :) - hejdav
55
文档和这里没有提到的是,但代码期望抛出异常的部分需要在expectException()之后调用。虽然对于一些人来说可能很明显,但这对我来说是个坑。 - Jason McCreary
19
从文档中并不明显,但是在你的函数抛出异常后,该函数之后的代码将不会被执行。因此,如果你想在同一个测试用例中测试多个异常,那么就不行了。 - laurent
显示剩余5条评论

131

在 PHPUnit 9 发布之前,你也可以使用 文档块注解

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

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


3
我认为,这是首选的方法。 - Mike Purcell
15
在我看来,异常“消息”和日志消息一样,不应该被测试。它们都被认为是在进行“手动”取证时提供的额外有用信息。需要测试的关键点是异常的“类型”。超出这个范围的任何内容都会过于紧密地绑定到实现中。只测试IncorrectPasswordException就足够了--消息等于“bob@me.com的密码错误”是附带的。此外,如果您想尽可能地少花时间编写测试,那么简单测试变得非常重要。 - David Harkness
9
@DavidHarkness,我想到会有人提到这一点。同样地,我认为总体上测试消息过于严格和紧密。然而,在某些情况下,例如规范的强制执行,正是这种严格性和紧密绑定可能是所需的(故意强调“可能”)。 - Levi Morrison
3
我不会去看文档块来理解它的期望,但我会查看实际的测试代码(无论是什么类型的测试)。这是所有其他测试的标准;我不认为_Exceptions_有合理的理由成为(哦天啊)这个惯例的例外。 - Kamafeather
6
“不要测试消息”规则听起来很合理,除非您测试一个方法,在代码的多个部分中引发相同的异常类型,唯一的区别是传递给消息的错误ID。 如果基于异常消息向用户显示消息,则会影响用户看到的具体消息内容,因此,您应该测试错误消息。 - Vanja D.
显示剩余7条评论

53

另一种方法可以是以下方式:

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

请确保您的测试类继承自\PHPUnit_Framework_TestCase


肯定这个语法中最甜的部分 - AndrewMcLagan
2
似乎 expectExceptionMessage 的行为类似于正则表达式。如果你的错误信息是 'Foo bar Baz',那么 $this->expectExceptionMessage('Foo'); 将使测试通过。 - Lucas Bustamante
2
这就是它!另外,expectExceptionCode(401) - darryn.ten
expectExceptionMessage($message) 只要实际的异常消息包含指定的消息,就可以正常工作。PHPUnit 内部使用 strpos 来评估条件。 - Sébastien

41

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->expect而不是$this->assert开始。已知一旦抛出异常,PHP就无法返回到抛出异常后的代码行。因此,您应该很容易地发现此测试中的错误。
<?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()应该是可以的,因为它在预期的确切异常类被抛出之前期望一个异常类,所以这里没有任何问题。

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

测试的输出为:

 ✔ Throw exception

OK (1 test, 1 assertion)

OK吗?

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

请注意输出:

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

测试计数正确,但只有一个1 assertion

应该有2个断言=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');
    }
}

测试结果出错:

通过了 (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有一个有用的机制称为数据提供程序。数据提供程序是一种返回数据(数组)及其数据集的方法。单个数据集用作测试方法-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.

如预期:

失败! 测试: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功能即可。

测试异常注意事项

类型为“TypeError”的异常

使用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

再次出现类型为“TypeError”的异常

我们知道,我们不需要使用if (!is_string($msg))来检查数据类型,因为如果我们在方法声明say(string $msg)中指定数据类型,PHP已经会处理这个操作。如果消息太长if (strlen($msg) > 3),我们可能希望抛出一个InvalidArgumentException

<?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,这样我们就有了两种情况(测试方法)会抛出异常 - 首先是当消息太长时的 testSayTooLong,其次是当消息类型错误时的 testSayDataType

在这两个测试中,我们期望抛出一个通用的 Exception 类而不是像 InvalidArgumentExceptionTypeError 这样的特定异常类,可以使用以下代码:

$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); 失败,并显示:

失败:期望异常 "Exception",实际上捕获到的却是 "TypeError"。

PHPUnit 抱怨 异常 TypeError 不是一个 Exception,否则在 testSayDataType() 中的 $this->expectException(\Exception::class); 就和 testSayTooLong() 中的 InvalidArgumentException 和期望值: $this->expectException(\Exception::class); 一样没问题了。

问题在于 PHPUnit 的描述会让你感到困惑,因为 TypeError 不是异常。 TypeError 没有扩展自 Exception 类也没有扩展自其他继承 Exception 的类。

TypeError 实现了 Throwable 接口,见 文档

InvalidArgumentException 扩展了 LogicException 文档

LogicException 扩展了 Exception 文档

因此 InvalidArgumentException 也扩展了 Exception

这就是为什么抛出 InvalidArgumentException 带着 $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


14
这是我见过的最大的答案。 - Oboroten
2
不确定是否应该标记为“将整个博客文章粘贴为答案”。 - clockw0rk

39

如果你在使用 PHP 5.5+,你可以使用 ::class 解析来获取类名,使用 expectException/setExpectedException。这提供了一些好处:

  • 名称将包含完整的命名空间(如果有)。
  • 它解析为一个字符串,因此它可以与任何版本的 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


37

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

重要提示:如果未抛出预期异常,它将失败。

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

7
我认为 $this->fail() 并不是用来这种方式的,至少目前不是(PHPUnit 3.6.11);它本身就像一个异常。以您的示例为例,如果调用 $this->fail("Expected exception not thrown"),那么 catch 块将被触发,并且 $e->getMessage() 的值为“Expected exception not thrown”。 - ken
1
@ken 你可能是对的。调用 fail 可能应该放在 catch 块之后,而不是 try 块内部。 - Frank Farmer
2
我必须点踩,因为fail的调用不应该在try块中。它本身会触发catch块,从而产生错误的结果。 - Twifty
7
我认为这种方法不能很好地处理某些情况是因为它使用了catch(Exception $e)来捕获所有异常。当我尝试捕获特定的异常时,这种方法对我来说非常有效: try { throw new MySpecificException; $this->fail('MySpecificException not thrown'); } catch(MySpecificException $e){} - spyle

25
你可以使用assertException extension来在一个测试执行中断言多个异常。
将该方法插入到你的TestCase中,然后使用它:
public function testSomething()
{
    $test = function() {
        // some code that has to throw an exception
    };
    $this->assertException( $test, 'InvalidArgumentException', 100, 'expected message' );
}

我还为喜欢优美代码的人编写了一个特性


你使用的是哪个PHPUnit版本?我正在使用PHPUnit 4.7.5,但是其中的assertException未定义。我在PHPUnit手册中也找不到它。 - physicalattraction
2
asertException 方法不是 PHPUnit 的原始方法。您必须继承 PHPUnit_Framework_TestCase 类并手动添加上面链接的方法。然后,您的测试用例将继承这个继承类。 - hejdav
太好了。我断言期望有两个异常,但phpunit的输出只显示了一个断言。 - Joel Mellon

21

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');
}
将其添加到您的测试类中,并以以下方式调用:
public function testSomething() {
    $this->assertException(\PDOException::class, function() {
        new \PDO('bad:param');
    });
    $this->assertException(\PDOException::class, function() {
        new \PDO('foo:bar');
    });
}

绝对是所有答案中最好的解决方案!将其放入特质(trait)并打包它! - domdambrogia
由于数据提供程序的支持,您可以在单个测试方法中使用 $this->expectException() 来测试多个异常。更多信息 - Jimmix

14

全面解决方案

PHPUnit当前的"最佳实践"在异常测试方面似乎有些平淡无奇 (文档)。

由于我需要更多的功能,而不仅仅是当前的expectException实现,所以我制作了一个特性用于我的测试案例。 它只有 ~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();
        });
    }
}

?>

6
有点讽刺的是,你的单元测试套件没有在代码库中包含单元测试。 - domdambrogia
3
@domdambrogia 感谢@jean-beguin的贡献,现在它已经拥有单元测试了。 - jchook
这似乎是断言某个方法不会抛出异常的唯一解决方案,这通常确实非常必要。 - Jannie Theunissen

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

1
assertEquals() 的签名是 assertEquals(mixed $expected, mixed $actual...),与您的示例相反,因此应该是 $this->assertEquals("Exception message", $ex->getMessage()); - Roger Campanera

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接