在 Mockery 中测试链式方法调用

问题描述 投票:0回答:4

我正试图在控制器中正确模拟对 Eloquent 模型的链式调用。在我的控制器中,我使用依赖注入来访问模型,以便模拟它应该很容易,但是我不确定如何测试链接调用并使其正常工作。这一切都在 Laravel 4.1 中使用 PHPUnit 和 Mockery。

控制器:

<?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 测试这个链式方法调用的最佳方法是什么。

php testing laravel phpunit mockery
4个回答
25
投票

最初是评论,但为了使代码清晰可读而移动到答案!

我也倾向于 @alexrussell 的回答,尽管中间立场是:

$this->mock->shouldReceive('remember->with->notSold->take->orderBy->get')
    ->andRe‌​turn($this->collection);

9
投票

我对自我测试还很陌生,在大多数人看来,这整个答案可能是错误的,但我确实看到人们普遍测试错误的东西。如果你准确地测试了一个方法所做的一切,那么你就不是在测试,而只是在写一个方法两次。

你应该把你的代码想象成一个黑盒子——当你写你的测试时不要假设知道里面发生了什么。使用给定输入调用方法,期望输出。有时您需要确保某些其他效果已经发生,这就是 shouldReceive 出现的时候。但它再次比这个收集链测试更高级别 - 您应该测试执行此代码所做的代码是否已完成,但是正是代码本身发生了。因此,集合链应该以某种方式提取到其他方法,您应该简单地测试该方法是否被调用。

你测试实际编写的代码(而不是代码的目的)的次数越多,你遇到的问题就越多。例如,如果您需要更新代码以不同的方式做同样的事情(也许

remember(6)
不是
remember(5)
作为该链的一部分或其他东西),您还必须更新您的测试以确保
remember(6)
是现在被称为,当你根本不应该测试它的时候。

这个建议当然不仅仅适用于链式方法,它适用于任何时候你确保在测试给定方法时各种对象调用了各种方法。

尽管我不喜欢“红色、绿色、重构”这个词,但您应该在这里考虑一下,因为您的测试方法有两点失败:

  • 红/绿:当你第一次写失败的测试时,你的代码不应该有所有这些
    shouldReceive
    s(如果有意义的话,也许一两个,见上文)——如果有,那么你就没有写一个测试,但你正在编写代码。实际上,这表明您先编写代码,然后再编写测试以适应代码,这与测试优先的 TDD 相悖。
  • 重构:假设您首先编写了代码,然后是适合代码的测试(或者嘿,以某种方式设法准确猜测应该在您的测试中编写什么代码只是神奇地完成了)。这很糟糕,但假设你做到了,因为这不是世界末日。你现在需要重构,但你不能不改变你的测试。您的测试与代码紧密耦合,任何重构都会破坏测试。也就是说,再次反对 TDD 的想法。

即使您不遵循测试优先的 TDD,您至少应该意识到重构步骤应该是可行的而不会破坏您的测试。

不管怎样,那只是我的小费。


5
投票

我发现了这种技术,但我不喜欢它。它非常冗长。我认为必须有一种更清洁/更简单的方法来实现这一目标。

在构造函数中:

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

0
投票

像下面这样尝试

$this->mock
->shouldReceive('remember->with->notSold->take->orderBy->get')
->once()
->andReturn('Any desired data');

希望这会奏效。谢谢。

© www.soinside.com 2019 - 2024. All rights reserved.