在Mockery中测试链式方法调用

Dwi*_*ght 8 php testing phpunit laravel mockery

我正在尝试在控制器中正确模拟对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'));
    }
}
Run Code Online (Sandbox Code Playgroud)

控制器测试:

<?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');
    }
}
Run Code Online (Sandbox Code Playgroud)

我一直试图通过$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);
Run Code Online (Sandbox Code Playgroud)

但是,这会导致错误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);
Run Code Online (Sandbox Code Playgroud)

使用Mockery测试这个链式方法调用应该采取的最佳方法是什么.

pet*_*les 21

最初的评论,但移动回答,使代码清晰可辨!

我也倾向于@ alexrussell的答案,尽管中间地带是:

$this->mock->shouldReceive('remember->with->notSold->take->orderBy->get')
    ->andRe??turn($this->collection);
Run Code Online (Sandbox Code Playgroud)


ale*_*ell 7

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

你应该把你的代码想象成一个黑盒子 - 不要假设你在编写测试时知道里面发生了什么.使用给定输入调用方法,期望输出.有时你需要确保发生了某些其他的影响,那就是当shouldReceive的东西进来的时候.但是它再次比这个集合链测试更高级 - 你应该测试代码来做这个代码所做的事情,但是正是代码本身发生了.因此,应该以某种方式将集合链提取到其他方法,并且您应该只测试该方法被调用.

您测试实际编写的代码(而不是代码的目的)越多,您将遇到的问题就越多.例如,如果您需要更新代码以不同的方式执行相同的操作(可能remember(6)不是remember(5)该链或其他内容的一部分),您还必须更新测试以确保remember(6)现在调用,当您不应该完全测试.

当然,这个建议并不仅仅适用于链式方法,只要您确保在测试给定方法时各种对象都有各种方法调用它们.

尽管我不喜欢"红色,绿色,重构"这个术语,但你应该在这里考虑它,因为有两点你的测试方法失败了:

  • 红色/绿色:当您第一次编写失败的测试时,您的代码不应该包含所有这些shouldReceive(如果有意义,可能是一两个,见上文) - 如果有,那么您不是在编写测​​试但是正在编写代码.实际上,这表明您首先编写代码然后测试代码以适应代码,这是针对测试优先的TDD.
  • 重构:假设你已经编写了代码,然后进行了测试以适应代码(或者嘿,他们设法猜测你的测试中应该写入的内容应该是什么,而代码只是神奇地计算出来了).这很糟糕,但是我们说你做到了,因为它不是世界末日.你现在需要重构,但你不能不改变你的测试.您的测试与代码密切相关,任何重构都会破坏测试.这也是反对TDD的想法.

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

无论如何,那只是我的傻瓜.


Dwi*_*ght 5

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

在构造函数中:

$this->collection = Mockery::mock('Illuminate\Database\Eloquent\Collection')->shouldDeferMissing();
Run Code Online (Sandbox Code Playgroud)

在测试中:

$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);
Run Code Online (Sandbox Code Playgroud)