【问题标题】:PHPUnit assert that an exception was thrown?PHPUnit 断言抛出异常?
【发布时间】:2011-08-06 17:12:07
【问题描述】:

有谁知道是否有assert 或类似的东西可以测试被测试的代码中是否抛出了异常?

【问题讨论】:

  • 对于那些答案:测试函数中的多断言怎么样,我只希望有一个抛出异常?我是否必须将它们分开并将其放入独立的测试功能中?
  • @PanwenWang 要测试多个异常或来自异常的 getter 的多个返回,请参阅 this answer

标签: php exception phpunit assert


【解决方案1】:
<?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 提供有关测试异常最佳实践的详细说明。

【讨论】:

  • 如果你使用命名空间,那么你需要输入完整的命名空间:$this-&gt;setExpectedException('\My\Name\Space\MyCustomException');
  • 您无法指定预期抛出的精确代码行这一事实是 IMO 错误。并且无法在同一个测试中测试多个异常,这使得测试许多预期的异常成为一件非常笨拙的事情。我写了一个actual assertion 来尝试解决这些问题。
  • 仅供参考:截至phpunit 5.2.0 setExpectedException 方法已弃用,替换为expectException 之一。 :)
  • 文档或此处未提及,但预期会引发异常的代码需要在 expectException() 之后调用。虽然这对某些人来说可能很明显,但对我来说这是一个陷阱
  • 从文档中看不出来,但是你的函数之后抛出异常的代码不会被执行。因此,如果您想在同一个测试用例中测试多个异常,则不能。
【解决方案2】:

在 PHPUnit 9 发布之前,您也可以使用 docblock annotation

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

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

【讨论】:

  • IMO,这是首选方法。
  • @LeviMorrison - 恕我直言,不应测试异常 message,类似于日志消息。在执行手动取证时,两者都被视为无关紧要的有用信息。测试的关键点是异常的type。除此之外的任何东西都与实现绑定得太紧。 IncorrectPasswordException 应该足够了——消息等于"Wrong password for bob@me.com" 是辅助的。再加上您希望尽可能少地编写测试,您就会开始看到简单测试变得多么重要。
  • @DavidHarkness 我想有人会提出来。同样,我同意测试消息通常过于严格和严格。然而,在某些情况下(例如执行规范),可能(有意强调)正是这种严格性和紧密绑定。
  • 我不会在文档块中观看以了解它的预期,但我会查看实际的测试代码(无论测试类型如何)。这是所有其他测试的标准;我没有看到 Exceptions 成为(天哪)这个约定的例外的正当理由。
  • “不测试消息”规则听起来是有效的,除非您测试的方法在多个代码部分中抛出相同的异常类型,唯一的区别是传递的错误 ID在消息中。您的系统可能会根据异常消息(不是异常类型)向用户显示消息。在这种情况下,用户看到的消息并不重要,因此,您应该测试错误消息。
【解决方案3】:

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

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

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

【讨论】:

  • $this-&gt;fail() 不应该以这种方式使用我不认为,至少目前不是(PHPUnit 3.6.11);它本身就是一个例外。使用您的示例,如果调用$this-&gt;fail("Expected exception not thrown"),则触发catch 块并且$e-&gt;getMessage()“未引发预期异常”
  • @ken 你可能是对的。对fail 的调用可能属于 catch 块之后,而不是在try 中。
  • 我必须投反对票,因为对fail 的调用不应该在try 块中。它本身会触发 catch 块产生错误结果。
  • 我相信这不能正常工作的原因是某些情况是它捕获了catch(Exception $e) 的所有异常。当我尝试捕获特定异常时,此方法对我来说效果很好:try { throw new MySpecificException; $this-&gt;fail('MySpecificException not thrown'); } catch(MySpecificException $e){}
【解决方案4】:

您可以在一次测试执行期间使用assertException extension 断言多个异常。

将方法插入您的 TestCase 并使用:

public function testSomething()
{
    $test = function() {
        // some code that has to throw an exception
    };
    $this->assertException( $test, 'InvalidArgumentException', 100, 'expected message' );
}

我还为漂亮代码的爱好者制作了一个trait..

【讨论】:

  • 您使用的是哪个 PHPUnit?我使用的是 PHPUnit 4.7.5,没有定义 assertException。我在 PHPUnit 手册中也找不到它。
  • asertException 方法不是原始 PHPUnit 的一部分。您必须继承PHPUnit_Framework_TestCase 类并手动添加method linked in post above。然后,您的测试用例将继承这个继承的类。
  • 这很棒。我断言我对两个异常的期望,而 phpunit 的输出只显示了一个断言。
【解决方案5】:
public function testException() {
    try {
        $this->methodThatThrowsException();
        $this->fail("Expected Exception has not been raised.");
    } catch (Exception $ex) {
        $this->assertEquals("Exception message", $ex->getMessage());
    }
    
}

【讨论】:

  • assertEquals()的签名是assertEquals(mixed $expected, mixed $actual...),和你的例子相反,所以应该是$this-&gt;assertEquals("Exception message", $ex-&gt;getMessage());
【解决方案6】:

如果您在 PHP 5.5+ 上运行,您可以使用::class resolution 来获取带有expectException/setExpectedException 的类的名称。这提供了几个好处:

  • 名称将完全限定其命名空间(如果有)。
  • 它解析为string,因此它适用于任何版本的 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 introducedexpectException 替代setExpectedException

【讨论】:

    【解决方案7】:
    /**
     * @expectedException Exception
     * @expectedExceptionMessage Amount has to be bigger then 0!
     */
    public function testDepositNegative()
    {
        $this->account->deposit(-7);
    }
    

    注意"/**",注意双“*”。只写“**”(asterix)会使你的代码失败。 还要确保您使用的是最新版本的 phpUnit。在一些早期版本的 phpunit 中不支持 @expectedException 异常。我有 4.0 但它对我不起作用,我必须更新到 5.5 https://coderwall.com/p/mklvdw/install-phpunit-with-composer 才能使用作曲家进行更新。

    【讨论】:

      【解决方案8】:

      另一种方法可以是:

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

      请确保您的测试类范围为\PHPUnit_Framework_TestCase

      【讨论】:

      • 肯定是这个语法中最糖的
      • 看来expectExceptionMessage 的行为类似于正则表达式。如果您的错误消息是“Foo bar Baz”,$this-&gt;expectExceptionMessage('Foo'); 将使测试通过。
      • 就是这个!还有,expectExceptionCode(401)
      【解决方案9】:

      这是您可以执行的所有异常断言。请注意,它们都是可选的

      class ExceptionTest extends PHPUnit_Framework_TestCase
      {
          public function testException()
          {
              // make your exception assertions
              $this->expectException(InvalidArgumentException::class);
              // if you use namespaces:
              // $this->expectException('\Namespace\MyExceptio‌​n');
              $this->expectExceptionMessage('message');
              $this->expectExceptionMessageRegExp('/essage$/');
              $this->expectExceptionCode(123);
              // code that throws an exception
              throw new InvalidArgumentException('message', 123);
         }
      
         public function testAnotherException()
         {
              // repeat as needed
              $this->expectException(Exception::class);
              throw new Exception('Oh no!');
          }
      }
      

      可以在here找到文档。

      【讨论】:

      • 这是不正确的,因为 PHP 在第一次抛出异常时停止。 PHPUnit 检查抛出的异常是否具有正确的类型并说“测试正常”,它甚至不知道第二个异常。
      【解决方案10】:

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

      【讨论】:

      • 绝对是所有答案中最好的解决方案!把它扔到一个特征中并打包它!
      • 感谢数据提供者,您可以使用$this-&gt;expectException() 每个测试方法测试多个异常。 More info
      【解决方案11】:

      综合解决方案

      PHPUnit 当前用于异常测试的“best practices”似乎……乏善可陈(docs)。

      由于我 wanted more 比当前的 expectException 实现,我创建了一个 trait 用于我的测试用例。只是~50 lines of code

      • 每个测试支持多个异常
      • 支持在抛出异常后调用断言
      • 强大而清晰的使用示例
      • 标准assert 语法
      • 不仅支持消息、代码和类的断言
      • 支持反向断言,assertNotThrows
      • 支持 PHP 7 Throwable 错误

      图书馆

      我将AssertThrows trait 发布到 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();
              });
          }
      }
      
      ?>
      

      【讨论】:

      • 有点讽刺的是,你的单元测试包在 repo 中没有包含单元测试。
      • @domdambrogia 感谢@jean-beguin 现在有单元测试。
      • 这似乎是断言某些方法不会抛出的唯一解决方案,这通常确实非常必要。
      【解决方案12】:

      对于 PHPUnit 5.7.27 和 PHP 5.6 并在一次测试中测试多个异常,强制异常测试很重要。如果没有异常发生,单独使用异常处理来断言 Exception 的实例将跳过测试情况。

      public function testSomeFunction() {
      
          $e=null;
          $targetClassObj= new TargetClass();
          try {
              $targetClassObj->doSomething();
          } catch ( \Exception $e ) {
          }
          $this->assertInstanceOf(\Exception::class,$e);
          $this->assertEquals('Some message',$e->getMessage());
      
          $e=null;
          try {
              $targetClassObj->doSomethingElse();
          } catch ( Exception $e ) {
          }
          $this->assertInstanceOf(\Exception::class,$e);
          $this->assertEquals('Another message',$e->getMessage());
      
      }
      

      【讨论】:

        【解决方案13】:
        function yourfunction($a,$z){
           if($a<$z){ throw new <YOUR_EXCEPTION>; }
        }
        

        这是测试

        class FunctionTest extends \PHPUnit_Framework_TestCase{
        
           public function testException(){
        
              $this->setExpectedException(<YOUR_EXCEPTION>::class);
              yourfunction(1,2);//add vars that cause the exception 
        
           }
        
        }
        

        【讨论】:

          【解决方案14】:

          PhpUnit 是一个了不起的库,但这一点有点令人沮丧。这就是为什么我们可以使用 turbotesting-php 开源库,它有一个非常方便的断言方法来帮助我们测试异常。可以在这里找到:

          https://github.com/edertone/TurboTesting/blob/master/TurboTesting-Php/src/main/php/utils/AssertUtils.php

          要使用它,我们只需执行以下操作:

          AssertUtils::throwsException(function(){
          
              // Some code that must throw an exception here
          
          }, '/expected error message/');
          

          如果我们在匿名函数中键入的代码没有抛出异常,就会抛出异常。

          如果我们在匿名函数中键入的代码抛出异常,但其消息与预期的正则表达式不匹配,也会抛出异常。

          【讨论】:

            【解决方案15】:

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

            但是Documentation 对测试代码中上述任何方法的顺序含糊不清。

            如果你习惯使用断言,例如:

            <?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-&gt;excpect 而不是 $this-&gt;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-&gt;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-&gt;expectException() 或混合使用 $this-&gt;expectException()$this-&gt;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');
                }
            }
            

            给出错误:

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

            因为一旦抛出异常,所有其他与测试异常相关的$this-&gt;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-&gt;fail($failMsg); 发出异常未引发的信号。

            使用 PHPUnit 的数据提供者

            PHPUnit 有一个有用的机制,称为Data Provider。数据提供者是一种返回带有数据集的数据(数组)的方法。当 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-&gt;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 已经注意到了,如果消息太多,我们可能想要抛出InvalidArgumentExceptionif (strlen($msg) &gt; 3)

            <?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,因此我们有两种情况(测试方法),其中应该抛出Exception - 第一个testSayTooLong,当消息太长时,第二个testSayDataType,当消息类型错误。

            在这两个测试中,我们期望通过使用而不是像 InvalidArgumentExceptionTypeError 这样的特定异常类,而只是使用通用 Exception

            $this-&gt;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-&gt;expectException(\Exception::class);

            InvalidArgumentException 被抛出时与OK 一起传递

            但是

            testSayDataType() 使用与描述相同的$this-&gt;expectException(\Exception::class); Fails

            未能断言“TypeError”类型的exception与预期的异常“Exception”匹配。

            PHPUnit 抱怨 exception TypeError 不是 Exception 看起来令人困惑,否则在 testSayDataType() 中的 $this-&gt;expectException(\Exception::class); 不会有任何问题,因为它没有testSayTooLong() 抛出 InvalidArgumentException 并期待:$this-&gt;expectException(\Exception::class);

            问题在于 PHPUnit 用上面的描述误导了你,因为TypeError 不是例外。 TypeError 不从 Exception 类或其任何其他子类扩展。

            TypeError 实现Throwable 接口见documentation

            InvalidArgumentException 扩展 LogicException documentation

            LogicException 扩展Exception documentation

            因此InvalidArgumentException 也扩展了Exception

            这就是为什么抛出 InvalidArgumentException 通过 OK 和 $this-&gt;expectException(\Exception::class); 的测试但抛出 TypeError 不会(它不会扩展 Exception

            但是ExceptionTypeError 都实现了Throwable 接口。

            因此在两个测试中都发生了变化

            $this-&gt;expectException(\Exception::class);

            $this-&gt;expectException(\Throwable::class);

            使测试变为绿色:

             ✔ Say too long
             ✔ Say data type
            
            OK (2 tests, 2 assertions)
            

            See the list of Errors and Exception classes and how they are related to each other.

            要明确一点:在单元测试中使用特定的异常或错误而不是通用的 ExceptionThrowable 是一个很好的做法,但是如果你现在遇到关于异常的误导性评论,你就会知道为什么 PHPUnit 的异常TypeError或其他异常错误其实不是Exceptions而是Throwable

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2019-09-03
              • 1970-01-01
              • 2012-04-28
              • 2017-03-09
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多