我正在尝试在控制器中正确模拟对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测试此链接方法调用时,我应采取的最佳方法是什么。
答案 0 :(得分:21)
最初是一条评论,但为了使代码清晰可见,我们接受了回答!
我也倾向于@alexrussell's answer,虽然中间地位是:
$this->mock->shouldReceive('remember->with->notSold->take->orderBy->get')
->andReturn($this->collection);
答案 1 :(得分:7)
我对测试自己很陌生,在大多数人的眼中,这整个答案可能都是错误的,但我确实看到人们普遍都在测试错误的东西。如果你测试一个方法所做的一切,那么你就不会测试,而只是编写一个方法两次。
你应该把你的代码想象成一个黑盒子 - 不要假设你在编写测试时知道里面发生了什么。使用给定输入调用方法,期望输出。有时你需要确保发生了某些其他的影响,那就是当shouldReceive的东西进来时。但是它再次比这个集合链测试更高级 - 你应该测试代码来完成这段代码所做的事情,但是正是代码本身发生了。因此,应该以某种方式将集合链提取到其他方法,并且您应该只测试该方法被调用。
您测试实际编写的代码(而不是代码的目的)越多,您将遇到的问题就越多。例如,如果您需要更新代码以不同的方式执行相同的操作(可能remember(6)
而非remember(5)
作为该链或其他内容的一部分),您还必须更新测试以确保现在调用remember(6)
,当你根本不应该测试它时。
这个建议当然不仅适用于链式方法,只要你确保在测试给定方法时各种对象都有各种方法调用它们。
尽管我不喜欢“红色,绿色,重构”这个术语,但你应该在这里考虑它,因为你的测试方法有两点失败:
shouldReceive
s(如果有意义,可能是一两个,见上文) - 如果有,那么你不是在写测试,而是在编写代码。实际上,这表明你先编写了代码,然后编写了适合代码的代码,这是针对测试优先的TDD。即使您不遵循测试优先TDD,您至少应该意识到重构步骤应该是可行的而不会破坏您的测试。
无论如何,那只是我的傻瓜。
答案 2 :(得分:2)
我发现了这种技术,但我并不喜欢它。它非常冗长。我认为必须有一种更清洁/更简单的方法来实现这一目标。
在构造函数中:
$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);