如何在 Laravel 中对调用另一个模型的方法的模型方法进行单元测试

Posted

技术标签:

【中文标题】如何在 Laravel 中对调用另一个模型的方法的模型方法进行单元测试【英文标题】:How to unit test model method that calls method on another model in Laravel 【发布时间】:2018-05-03 05:51:14 【问题描述】:

我是测试和编写可测试代码的新手,我正在寻找有关处理这个简单场景的正确方法的一些说明。我已经阅读了关于 SO 的其他类似标题的问题和答案,但它们似乎没有为我的问题提供明确的答案。

我有一个控制器,它在我的 Picking 类的实例上调用 shipped() 方法:

class MyController extends \BaseController 

    public function controllerMethod() 
        $picking = new Picking;
        $picking->shipped($shipmentData);
    

Picking 模型如下所示:

class Picking extends \Eloquent 

    public function order() 
        return $this->belongsTo('Order');
        

    public function shipped($shipmentData) 
        $this->carrier = $shipmentData['Carrier'];
        $this->service = $shipmentData['Service'];
        $this->is_shipped = true;
        $this->save();

        $this->order->pickingShipped();
    

如你所见,这个shipped()方法保存了一些数据,然后调用pickingShipped()方法,在它的相关Order上。

现在,我正在尝试为 shipped() 方法编写测试,但我不确定执行此操作的适当方法。我读过关于模拟的文章,但如果这是需要模拟的情况,我会感到困惑。我已经想到了一些可能的解决方案,但我不确定它们是否正确。

1) 重新排列代码,以便控制器调用pickingShipped() 方法,允许将其从shipped() 方法中删除,从而简化测试。

例如,shipped() 方法的最后一行将被删除,控制器代码将更改为:

$picking = new Picking;
$picking->shipped($shipmentData);
$picking->order->pickingShipped();

2) 在测试中,对order 使用模拟方法,以便测试可以简单地确认pickingShipped() 方法被调用。

类似于here 解释的内容。这意味着测试可以做这样的事情:

$order->expects($this->once())->method('pickingShipped')

但是,我认为这意味着我还需要注入订单依赖项,而不是依赖 shipped() 方法中的 order 关系,如下所示:

class Picking extends \Eloquent 

    public function order() 
        return $this->belongsTo('Order');
        

    public function shipped(Order $order, $shipmentData) 
        $this->carrier = $shipmentData['Carrier'];
        $this->service = $shipmentData['Service'];
        $this->is_shipped = true;
        $this->save();

        $order->pickingShipped();
    

然后控制器中的代码必须如下所示:

$picking = new Picking;
$picking->shipped($picking->order, $shipmentData);

这感觉有点奇怪,但我真的不确定什么是对的。

我的问题是,编写和测试这段代码的正确方法是什么?很容易测试 shipped() 方法在其自身上设置适当的数据,但是最后对 pickingShipped() 的调用呢?这似乎使测试更加复杂。那么代码应该重新排列吗?如果是这样,怎么做?或者,这是我在第二个选项中概述的模拟的常见用例吗?如果是这样,像我展示的那样注入依赖项是否正确?

【问题讨论】:

【参考方案1】:

我不是 php 开发人员,所以这可能归结为语言功能成为障碍。

我建议依赖注入方法更好,因为它会调用依赖并允许您稍后将持久性和行为分开。例如PickingPicker 可能是更好的行为名称,而PickingRecord 可能更适合数据。

无论如何,如果您可以在 PHP 中设置默认参数,那么我喜欢您使用的最后一种方法(注入),您目前可以简化为类似

   public function shipped($shipmentData, Order $order = $this->order) 
    $this->carrier = $shipmentData['Carrier'];
    $this->service = $shipmentData['Service'];
    $this->is_shipped = true;
    $this->save();

    $order->pickingShipped();

这将允许您在生产代码中忽略 order 依赖项,并在测试中将 double 或其他类型的对象作为 order 注入,并简单地断言在 order 对象上调用了该方法。即使您在单元测试中注入双打,集成测试仍应继续监控接口是否仍然啮合在一起。

这就是我尝试在 Ruby 中执行此操作的方式。

【讨论】:

感谢您的反馈!我对这种方法的唯一问题是,我们允许将 $order 传递到方法中,而实际上这是不允许的。该方法的重点是它必须在拣货订单上调用pickingShipped,而不仅仅是任何订单。这意味着我们只是为了测试目的而添加这个参数。仅仅为了测试目的而改变方法签名感觉很奇怪。现在该方法建议它应该用于实际上不应该的行为。 关于默认参数,是的,PHP 允许您传入它们,但它们不能具有像 $this->order 这样的动态值。在这种情况下,我会将默认值设置为null,然后在方法的开头说if (is_null($order)) $order = $this->order;,这将完成同样的事情,但就像我上面所说的,这种依赖注入方法仍然感觉不对对我来说。【参考方案2】:

我想出了一个我觉得很好的解决方案。现在我看到它似乎很明显。我所做的只是设置$picking->order 属性以返回测试的模拟顺序。

$order = Mockery::mock(Order::class);

$picking = new Picking;
$picking->order = $order;

$order->shouldReceive('pickingShipped')
  ->with($picking)
  ->once();

$picking->shipped($shipmentData);

现在当shipped() 方法调用$this->order 时,它得到了我定义的模拟$order 对象,并且测试正常工作。

这感觉是正确的解决方案。

【讨论】:

以上是关于如何在 Laravel 中对调用另一个模型的方法的模型方法进行单元测试的主要内容,如果未能解决你的问题,请参考以下文章

如何在laravel中对相关模型进行分页

如何在 Laravel 5 中对合并的集合进行分页?

Laravel - 如何调用模型类助手然后在视图中显示结果

如何使用newQuery()在Laravel中对表进行别名

如何从迁移中创建 Laravel 模型?

如何测试 Laravel 5.4 中的“创建”事件是不是调用了 Eloquent 模型方法?