在 Mockery 中测试链式方法调用
Posted
技术标签:
【中文标题】在 Mockery 中测试链式方法调用【英文标题】:Testing chained method call in Mockery 【发布时间】:2014-03-11 16:41:30 【问题描述】:我正在尝试在控制器中正确模拟对 Eloquent 模型的链式调用。在我的控制器中,我使用依赖注入来访问模型,以便它应该易于模拟,但是我不确定如何测试链接调用并使其正常工作。这一切都在使用 phpUnit 和 Mockery 的 Laravel 4.1 中。
控制器:
<?php
class TextbooksController extends BaseController
protected $textbook;
public function __construct(Textbook $textbook)
$this->textbook = $textbook;
public function index()
$textbooks = $this->textbook->remember(5)
->with('user')
->notSold()
->take(25)
->orderBy('created_at', 'desc')
->get();
return View::make('textbooks.index', compact('textbooks'));
控制器测试:
<?php
class TextbooksControllerText extends TestCase
public function __construct()
$this->mock = Mockery::mock('Eloquent', 'Textbook');
public function tearDown()
Mockery::close();
public function testIndex()
// Here I want properly mock my chained call to the Textbook
// model.
$this->action('GET', 'TextbooksController@index');
$this->assertResponseOk();
$this->assertViewHas('textbooks');
我一直试图通过将此代码放在测试中的 $this->action()
调用之前来实现这一点。
$this->mock->shouldReceive('remember')->with(5)->once();
$this->mock->shouldReceive('with')->with('user')->once();
$this->mock->shouldReceive('notSold')->once();
$this->app->instance('Textbook', $this->mock);
但是,这会导致错误Fatal error: Call to a member function with() on a non-object in /app/controllers/TextbooksController.php on line 28
。
我还尝试了一种链式替代方案,希望它能解决问题。
$this->mock->shouldReceive('remember')->with(5)->once()
->shouldReceive('with')->with('user')->once()
->shouldReceive('notSold')->once();
$this->app->instance('Textbook', $this->mock);
使用 Mockery 测试这个链式方法调用的最佳方法是什么。
【问题讨论】:
请阅读文档github.com/padraic/… 【参考方案1】:最初是一条评论,但为了使代码清晰易读而转为回答!
我也倾向于@alexrussell's answer,但中间立场是:
$this->mock->shouldReceive('remember->with->notSold->take->orderBy->get')
->andReturn($this->collection);
【讨论】:
这行得通,但我注意到它会导致代码覆盖失败(如果你正在使用它) 我不知道,所以感谢您指出。另一个不被卷入被测单元内部的原因:) @petercoles 3 年后,但 $this->mock 不可用。如何实例化? OP 在他的(稍微错误命名的)TextbooksControllerTest 中的构造函数中设置了 $this->mock,这可能是为了避免在 setUp 方法中重复执行它,尽管我个人仍然更喜欢后者.【参考方案2】:我对测试自己很陌生,整个答案在大多数人眼中可能是错误的,但我确实看到人们测试错误事物的普遍性。如果您完全测试方法所做的所有事情,那么您不是在测试,而只是将方法编写了两次。
您应该将您的代码视为一个黑匣子——当您编写测试时,不要假定知道里面发生了什么。调用具有给定输入的方法,期望输出。有时你需要确保某些其他效果已经发生,这就是 shouldReceive 东西进来的时候。但它再次比这个集合链测试更高级别 - 你应该测试代码来完成这个代码所做的事情,但是正是代码本身发生的。因此,应该以某种方式将集合链提取到其他方法,并且您应该简单地测试该方法是否被调用。
您对实际编写的代码(而不是代码的目的)进行测试的次数越多,您遇到的问题就越多。例如,如果您需要更新代码以不同的方式执行相同的操作(可能是 remember(6)
而不是 remember(5)
作为该链的一部分或其他东西),您还必须更新您的测试以确保 remember(6)
是现在调用,当你根本不应该测试它时。
当然,此建议不仅仅适用于链式方法,它适用于任何时候您确保在测试给定方法时各种对象都有调用它们的各种方法。
尽管我不喜欢“红色、绿色、重构”这个术语,但您应该在这里考虑一下,因为您的测试方法有两点失败:
红/绿:当你第一次编写失败的测试时,你的代码不应该有所有这些shouldReceive
s(如果有意义的话,可能只有一两个,见上文)——如果有,那么你就是不是在写测试,而是在写代码。实际上,这表明您先编写代码,然后再编写测试以适应代码,这违反了测试优先的 TDD。
重构:假设您首先编写了代码,然后进行了适合代码的测试(或者,嘿不知何故设法准确地猜测应该在您的测试中写什么,代码只是神奇地解决了)。这很糟糕,但假设你做到了,因为这不是世界末日。你现在需要重构,但你不能不改变你的测试。您的测试与代码紧密耦合,任何重构都会破坏测试。这再次与 TDD 的理念背道而驰。
即使您不遵循测试优先 TDD,您至少应该意识到重构步骤应该是可行的,而不会破坏您的测试。
无论如何,这只是我的遗憾。
【讨论】:
另外我知道这并不能直接回答所问的问题,但我认为这是一个很好的答案,因为它可以回答代码的更广泛的问题,并且对社区有帮助大。 是的,这是一个很好的答案。实际上通读它让我觉得有点愚蠢,因为它现在看起来更明显了;测试最终结果是否符合预期,并且仅在代码的更高级别对该过程至关重要时才测试它们。我将在下面留下我的答案,因为它在技术上确实达到了我最初寻找的结果,但这是错误的方法。 我们一起回答问题的两面 :) 不要直接使用控制器中的模型,而是使用存储库类并检查控制器是否按照应有的方式调用它:culttt.com/2013/07/08/… 然后,应使用集成测试(由于@alexrussell 在他的回答中解释的原因,而不是进行单元测试)。【参考方案3】:我发现了这种技术,但我不喜欢它。它非常冗长。我认为必须有一种更清洁/更简单的方法来实现这一点。
在构造函数中:
$this->collection = Mockery::mock('Illuminate\Database\Eloquent\Collection')->shouldDeferMissing();
在测试中:
$this->mock->shouldReceive('remember')->with(5)->andReturn($this->mock);
$this->mock->shouldReceive('with')->with('user')->andReturn($this->mock);
$this->mock->shouldReceive('notSold')->andReturn($this->mock);
$this->mock->shouldReceive('take')->with(25)->andReturn($this->mock);
$this->mock->shouldReceive('orderBy')->with('created_at', 'DESC')->andReturn($this->mock);
$this->mock->shouldReceive('get')->andReturn($this->collection);
【讨论】:
以上是关于在 Mockery 中测试链式方法调用的主要内容,如果未能解决你的问题,请参考以下文章