【问题标题】:How do I mock an object I can't pass to a method to trigger an exception?如何模拟无法传递给方法以触发异常的对象?
【发布时间】:2017-05-01 08:48:26
【问题描述】:

考虑以下方法:

function m1()
{
    $ent = new Entity;
    ...
    try {
        $ent->save();
    } catch (QueryException $e) {
        ...
    }

我必须触发一个异常。最好使用mockery。我该怎么做?

附:我无法将$ent 传递给方法。

UPD 让我描述一下我的特殊情况,以确认我是否确实需要触发异常。在这里,我正在尝试测试由支付系统触发的控制器操作,以通知用户已付款。在其中,我在数据库中存储来自PaymentSystemCallback 模型的所有支付系统数据,并将其链接到Order 模型,该模型是在将用户重定向到支付系统之前创建的。所以,它是这样的:

function callback(Request $request)
{
    $c = new PaymentSystemCallback;
    $c->remote_addr = $request->ip();
    $c->post_data = ...;
    $c->headers = ...;
    ...
    $c->save();

    $c->order_id = $request->request->get('order_id');
    $c->save();
}

但是如果不正确的order_id进来了,外部约束就失效了,所以我这样改:

try {
    $c->save();
} catch (QueryException $e) {
    return response('', 400);
}

但是以这种方式处理任何数据库异常看起来并不好,所以我正在寻找一种方法来重新抛出异常,除非$e->errorInfo[1] == 1452

【问题讨论】:

    标签: php phpunit automated-tests mockery


    【解决方案1】:

    您的方法不是为测试而设计的。修复它。如果你不能,那么你必须猴子补丁,PHP does not support natively

    我推荐的方法是让您的测试套件安装自己的优先自动加载器。让您的测试用例在该自动加载器中注册一个模拟类,并与类名Entity 关联。你的模拟类会发挥它的魔力来抛出异常。如果您使用的是 PHP 7,则可以访问匿名类,这使固定装置更容易:new class Entity {}

    根据公认的答案,Mockery 在模拟类上使用 overload: 量词支持这种自动加载技巧。这为您节省了大量工作!

    【讨论】:

      【解决方案2】:

      解决此问题的最简单方法是调用创建实体模拟实例的工厂方法。比如:

      function testSomething()
      {
          $ent = $this->getEntity();
          ...
          try {
              $ent->save();
          } catch (QueryException $e) {
              ...
          }
      }
      
      function getEntity()
      {
          $mock = $this->createMock(Entity::class);
          $mock
              ->method('save')
              ->will($this->throwException(new QueryException));
      
          return $mock;
      }
      

      【讨论】:

      • $mock 如何进入 SUT testSomething
      • 通过调用 $ent = $this->getEntity();从您的测试方法中(在本例中为 testSomething()
      • 所以 OP 必须更改被测方法才能使其正常工作?
      • 从 $ent = new Entity 到 $ent = $this->getEntity 几乎没有什么大的变化。有什么问题?
      • 好吧,也许 OP 不控制库。鉴于 OP 声明“他无法将对象传入”,我认为得出 OP 也无法修改内部结构的结论是合理的。
      【解决方案3】:

      这就是我想出的:

      /**
       * @runInSeparateProcess
       * @preserveGlobalState disabled
       */
      function testExceptionOnSave()
      {
          $this->setUpState();
      
          Mockery::mock('overload:App\PaymentSystemCallback')
              ->shouldReceive('save')
              ->andReturnUsing(function() {}, function() {
                  throw new QueryException('', [], new Exception);
              });
      
          $this->doRequest();
      
          $this->assertBalanceDidntChange();
          $this->assertNotProcessed();
          $this->seeStatusCode(500);
      }
      

      我使用 @runInSeparateProcess 是因为前面的测试触发了相同的操作,因此在 mockery 有机会模拟它之前加载了该类。

      至于@preserveGlobalState disabled,没有它就不行。正如phpunit's documentation 所说:

      注意:默认情况下,PHPUnit 将尝试通过序列化父进程中的所有全局变量并在子进程中反序列化它们来保留父进程的全局状态。如果父进程包含不可序列化的全局变量,这可能会导致问题。有关如何解决此问题的信息,请参阅名为“@preserveGlobalState”的部分。

      当我只标记一个测试以在单独的进程中运行时,我与mockery's documentation 所说的略有不同,因为我只需要它进行一个测试。不是全班。

      欢迎批评。

      【讨论】:

      • 我没有抱怨。这很清楚,也很重要。在底层,它使用自动加载技巧将模拟连接到目标类。
      猜你喜欢
      • 2016-02-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-02-05
      • 2018-07-22
      • 2015-05-24
      • 2015-03-08
      相关资源
      最近更新 更多