【问题标题】:PHPUnit doesn't continue test after expecting an exceptionPHPUnit 在预期异常后不继续测试
【发布时间】: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”测试。我需要为每个抛出的异常进行单独的测试吗?

【问题讨论】:

    标签: php exception phpunit


    【解决方案1】:

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

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

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

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

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

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

    3. 这个特殊的例子看起来是使用 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。

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

        我推荐阅读它。

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

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

        运行测试
        $ phpunit --testdox
        

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

        基于您的问题的示例

        注意我在这个例子中提供的 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);
        }
        

        注意我也建议看看

        【讨论】:

          【解决方案6】:

          首先,有一个错字。替换

          __DIR

          __DIR__

          :)


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

          再次感谢@SDC

          【讨论】:

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