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

ssm49v7z  于 2022-12-25  发布在  PHP
关注(0)|答案(7)|浏览(152)

为什么PHPUnit不执行这段代码中的最后一个异常Assert?

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"测试。是否需要对抛出的每个异常分别进行测试?

wnrlj8wa

wnrlj8wa1#

即使使用setExpectedException,您的测试仍然是常规的PHP代码,并且遵循PHP的常规规则。如果抛出异常,程序流将立即跳出当前上下文,直到到达try/catch块。
在PHPUnit中,当你使用setExpectedException时,它会告诉PHPUnit的核心什么时候应该从即将运行的代码中期待一个异常,因此它会用一个try/catch块等待它,如果catch被用它期待的异常类型调用,它就通过测试。
但是,在您的测试方法中,正常的PHP规则仍然适用--当异常发生时,当前代码块就结束了。除非您在测试方法中有自己的try/catch代码块,否则不会执行该方法中的任何其他代码块。
因此,为了测试多个异常,您有几个选项:
1.将您自己的try/catch添加到测试方法中,以便在第一个异常之后可以在该方法中进行进一步的测试。
1.将测试拆分为单独的方法,以便每个异常都在自己的测试中。
1.这个特定的示例看起来是使用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."),
     );
 }
bkhjykvo

bkhjykvo2#

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

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

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

1zmg4dgp3#

如果你需要在抛出异常后执行额外的Assert,只需要使用这个模板:

//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;
    }
kyks70gy

kyks70gy4#

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

$exception_thrown = false

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

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

cqoc49vn5#

在@SDC回答的基础上,我提出以下建议

  • 将测试进一步分开
  • 避免使用示例属性引用测试中的系统

进一步拆分测试

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

避免使用SUT的示例属性

当我开始编写测试时,我觉得在setUp中安排被测系统(SUT),然后在各个测试中通过相应的示例属性引用SUT时,有机会减少代码重复。
这很诱人,但是过了一段时间,当您开始从SUT中提取协作者时,您将需要设置测试副本,在开始时这可能对您仍然有效,但是之后您将开始在不同的测试中 * 以不同的方式 * 设置测试副本,并且所有先前旨在避免的重复都将回到您的身上:您将结束设置两个测试副本,并再次在测试中安排SUT。
当我在代码评审中遇到这种情况时,我喜欢引用

我推荐你阅读一读。
重要的一点是,您希望让编写 * 和 * 维护测试变得容易。(或者任何代码,如果你愿意的话)意味着让代码更容易阅读。如果你读了一些代码,比如说,一个类方法,你想容易地理解它是关于什么的,理想的情况下,这个方法应该做你期望它从类名做的事情。如果你测试不同的行为,通过创建不同的测试方法使其显而易见。
这样做的另一个好处是,如果您使用

$ phpunit --testdox

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

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

基于您的问题的示例

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

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

注意我还建议您查看

hfsqlsce

hfsqlsce6#

首先,有一个打字错误。替换
第一个月

__DIR__
:)
感谢@SDC的评论,我意识到你确实需要为每个异常单独的测试方法(如果你使用PHPUnit的expectedException特性)。你代码的第三个Assert没有被执行。如果你需要在一个测试方法中测试多个异常,我建议你在测试方法中编写你自己的trycatch语句。
再次感谢@SDC

cfh9epnr

cfh9epnr7#

我有这个特质

public function assertWeGotException($callback,$expectedExceptionClass = null){
        $gotException = false;
        try {
            $callback();
        }catch (\Exception $e){
            $gotException = true;
            if (!empty($expectedExceptionClass)){
                $exceptionGottenClass = get_class($e);
                $this->assertTextEquals($expectedExceptionClass,$exceptionGottenClass,'We got a different Exception');
            }
        }
        $this->assertTrue($gotException,'We got no exception');
    }

我是这样称呼它的:

$this->assertWeGotException(function (){
        // Code that throws any exception here
        throw new \Exception();
    });

    $this->assertWeGotException(function (){
        // Code that throws a RuntimeException here
        throw new \RuntimeException();
    },'RuntimeException');

相关问题