31

为什么 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”测试。我需要为每个抛出的异常进行单独的测试吗?

4

6 回答 6

57

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

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

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

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

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

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

  3. 这个特殊的例子看起来是使用 PHPUnitdataProvider机制的一个很好的例子,因为你基本上是在用两组数据测试相同的功能。该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."),
        );
    }
    

希望有帮助。

于 2013-01-28T13:45:45.460 回答
26

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

这是我的实现:

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

try {
    $report = $service->executeReport($reportId, $jobId);
} finally {
    $this->assertEquals($report->getStatus(), StatusMapper::STATUS_ABORTED);
}
于 2017-08-26T08:46:42.680 回答
5

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

    //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;
    }
于 2018-11-15T17:01:20.673 回答
4

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

$exception_thrown = false

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

$this->assertSame(true, $exception_thrown);
于 2016-08-18T11:07:29.000 回答
2

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

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

进一步拆分测试

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

避免使用 SUT 的实例属性

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

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

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

我建议阅读它。

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

这也有一个好处,如果你运行你的测试

$ phpunit --testdox

您最终会得到一个很好的预期行为列表,请参阅

基于您的问题的示例

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

/**
 * 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);
}

注意我也建议看看

于 2017-08-28T12:38:43.887 回答
0

首先,有一个错字。代替

__DIR

__DIR__

:)


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

再次感谢@SDC

于 2013-01-28T12:23:51.497 回答