PHPUnit 在预期异常后不继续测试

Posted

技术标签:

【中文标题】PHPUnit 在预期异常后不继续测试【英文标题】:PHPUnit doesn't continue test after expecting an exception 【发布时间】:2013-01-11 18:48:59 【问题描述】:

为什么 phpUnit 在这段代码中不做最后一个异常断言?

public function testConfigOverriding()

    $this->dependencyContainer = new DependencyContainer(__DIR__ . "/../../Resources/valid_json.json");
    $this->assertEquals('overriden', $this->dependencyContainer->getConfig('shell_commander')['pygmentize_command']);

    $unexisting = "unexisting_file";
    $this->setExpectedException('Exception', "Configuration file at path \"$unexisting\" doesn't exist.");
    $this->dependencyContainer = new DependencyContainer($unexisting);

    $invalid = __DIR . "/../../Resources/invalid_json.json";
    $this->setExpectedException('Exception', "Configuration JSON file provided is not valid.");
    $this->dependencyContainer = new DependencyContainer($invalid);

所以基本上:它测试是否抛出了“unexsisting_file”异常,但完全忽略了“invalid json”测试。我需要为每个抛出的异常进行单独的测试吗?

【问题讨论】:

【参考方案1】:

即使使用setExpectedException,您的测试仍然是常规的 PHP 代码,并且遵循 PHP 的常规规则。如果抛出异常,程序流会立即跳出当前上下文,直到到达try/catch 块。

在 PHPUnit 中,当您使用 setExpectedException 时,它会告诉 PHPUnit 的核心,它应该在即将运行的代码中期待异常。因此,它会使用 try/catch 块等待它,如果 catch 被调用并带有预期的异常类型,则通过测试。

但是,在您的测试方法中,正常的 PHP 规则仍然适用——当异常发生时,当前代码块就结束了。除非您在测试方法中有自己的 try/catch 块,否则不会执行该方法中的任何其他内容。

因此,为了测试多个异常,您有几个选择:

    将您自己的try/catch 添加到测试方法中,这样您就可以在第一个异常之后在该方法中继续进行进一步的测试。

    将测试拆分为单独的方法,以便每个异常都在自己的测试中。

    这个特殊的例子看起来是使用 PHPUnit 的dataProvider 机制的一个很好的例子,因为你基本上是在用两组数据测试相同的功能。 dataProvider 功能允许您定义一个单独的函数,该函数包含要测试的每组值的输入数据数组。然后将这些值一次一组传递到测试方法中。您的代码将如下所示:

    /**
     * @dataProvider providerConfigOverriding
     */
    public function testConfigOverriding($filename, $expectedExceptionText) 
        $this->dependencyContainer = new DependencyContainer(__DIR__ . "/../../Resources/valid_json.json");
        $this->assertEquals('overriden', $this->dependencyContainer->getConfig('shell_commander')['pygmentize_command']);
    
        $this->setExpectedException('Exception', $expectedExceptionText);
        $this->dependencyContainer = new DependencyContainer($filename);
    
    
    public function providerConfigOverriding() 
        return array(
            array('unexisting_file', 'Configuration file at path "unexisting_file" doesn\'t exist.'),
            array(__DIR__ . "/../../Resources/invalid_json.json", "Configuration JSON file provided is not valid."),
        );
    
    

希望对您有所帮助。

【讨论】:

如果可以的话会给你+2 ;)【参考方案2】:

我发现在异常发生后继续测试的最简单方法是在测试中实现 try/finally 块。这实质上允许测试的执行继续进行,而不管抛出任何异常。

这是我的实现:

$this->expectException(InvalidOperationException::class);

try 
    $report = $service->executeReport($reportId, $jobId);
 finally 
    $this->assertEquals($report->getStatus(), StatusMapper::STATUS_ABORTED);

【讨论】:

我喜欢这个解决方案,它可以解决问题并提醒您仍然抛出异常 迄今为止最优雅的解决方案 简单优雅的解决方案 爱它。这应该是#1【参考方案3】:

如果您需要在抛出异常后执行额外的断言,只需使用此模板:

    //You can use annotations instead of this method
    $this->expectException(FooException::class);

    try 
        $testable->setFoo($bar);
     catch (FooException $exception) 
        //Asserting that $testable->foo stays unchanged
        $this->assertEquals($foo, $testable->getFoo());
        //re-throwing exception
        throw $exception;
    

【讨论】:

【参考方案4】:

对于任何想要做问题标题中的事情的人,这是我想出的最干净的事情。

$exception_thrown = false

try 
    ... stuff that should throw exception ...
 catch (SomeTypeOfException $e) 
    $exception_thrown = true;


$this->assertSame(true, $exception_thrown);

【讨论】:

【参考方案5】:

基于@SDC 的回答,我推荐以下内容

进一步拆分测试 避免使用实例属性来引用被测系统

进一步拆分测试

如果断言与相同的行为不相关,则单个测试中的多个断言会出现问题:您无法正确命名测试,甚至可能最终在测试方法名称中使用and。如果发生这种情况,请将测试拆分为单独的测试

避免使用 SUT 的实例属性

当我开始编写测试时,我觉得在 setUp 中安排被测系统 (SUT) 并在各个测试中通过相应的实例属性引用 SUT 时,有机会减少代码重复。

这很诱人,但过了一段时间,当您开始从 SUT 中提取协作者时,您将需要设置测试替身。一开始这可能对您仍然有效,但随后您将开始在不同的测试中以不同方式设置测试替身,而之前旨在避免的所有重复都会回到您身上:您最终会设置两个测试替身,并在您的测试中再次安排 SUT。

当我在代码审查中遇到这个时,我喜欢参考

What does “DAMP not DRY” mean when talking about unit tests?

我推荐阅读它。

重要的一点是,您想让编写测试变得容易。维护测试(或任何代码,如果你愿意的话)主要意味着使代码易于阅读。如果您阅读一些代码,比如说,一个类方法,您想轻松理解它的含义,理想情况下,该方法应该按照您的类名做您期望它做的事情。如果您正在测试不同的行为,请通过创建不同的测试方法使其显而易见。

这还有一个好处是,如果您使用

运行测试
$ phpunit --testdox

您最终会得到一份不错的预期行为列表,请参阅

https://phpunit.de/manual/current/en/other-uses-for-tests.html#other-uses-for-tests.agile-documentation

基于您的问题的示例

注意我在这个例子中提供的 cmets 只是为了说明进一步拆分测试的想法,在实际代码中我不会有它们。

/**
 * The name of this method suggests a behaviour we expect from the
 * constructor of DependencyContainer
 */
public function testCanOverrideShellCommanderConfiguration()

    $container = new DependencyContainer(__DIR__ . '/../../Resources/valid_json.json');

    $this->assertEquals(
        'overriden', 
        $container->getConfig('shell_commander')['pygmentize_command']
    );


/**
 * While the idea of using a data provider is good, splitting the test
 * further makes sense for the following reasons
 *
 * - running tests with --testdox option as lined out above
 * - modifying the behaviour independently 
 *     Currently, a generic Exception is thrown, but you might 
 *     consider using a more specific exception from the SPL library, 
 *     (see http://php.net/manual/en/spl.exceptions.php), 
 *     or creating your own NonExistentConfigurationException class, 
 *     and then a data provider might not make sense anymore)
 */
public function testConstructorRejectsNonExistentConfigurationFile()

    $path = 'unexisting_file';

    $this->setExpectedException(\Exception::class, sprintf(
        'Configuration file at path "%s" doesn\'t exist.',
        $path
    ));

    new DependencyContainer($path);


public function testConstructorRejectsInvalidConfigurationFile()

    $path = __DIR__ . '/../../Resources/invalid_json.json';

    $this->setExpectedException(
        \Exception::class, 
        'Configuration JSON file provided is not valid.'
    );

    new DependencyContainer($path);

注意我也建议看看

https://thephp.cc/news/2016/02/questioning-phpunit-best-practices

【讨论】:

【参考方案6】:

首先,有一个错字。替换

__DIR

__DIR__

:)


感谢@SDC 的评论,我意识到您确实需要为每个异常提供单独的测试方法(如果您使用的是 PHPUnit 的 expectedException 功能)。您的代码的第三个断言只是没有被执行。如果您需要在一种测试方法中测试多个异常,我建议您在测试方法中编写自己的 try catch 语句。

再次感谢@SDC

【讨论】:

确实,很好,但是如果执行了该代码,我应该在输出中得到一个错误。它只是不运行任何低于第一个预期异常的东西。 在修正错字之后? 好的。将在本地准备测试 这里是已经准备好的链接:dropbox.com/s/601oxl2lu4afurl/PHPygmentizator.tar.gz 这样就不用再写了。 抱歉,但在此处给出的示例中,永远不会测试第二个异常。测试将通过,因为第一个异常在预期时抛出,但此时测试方法将停止,因为它已抛出异常。这可能是意料之中的,但它并没有被包含在测试方法中;它在 PHPUnit 中被进一步捕获,因此测试方法的其余部分将永远不会运行。您可以通过不期待第二个例外来证明这一点。测试仍然会通过,因为没有调用 throwExceptionB。您需要两种测试方法或测试方法中的try/catch

以上是关于PHPUnit 在预期异常后不继续测试的主要内容,如果未能解决你的问题,请参考以下文章

如何在无类 php 文件上使用 PHPUnit Skeleton Generator?

PhpStorm 配置 PHPUnit

phpunit 测试 assertNotRegExp() 没有按预期工作?

PHPUnit:预期状态码 200 但使用 Laravel 收到 419

为啥当我运行我的 phpunit 测试时,我得到一个空响应,但 Postman 中的相同路由/用户的行为符合预期?

PhpUnit 点击提交后不等待加载页面