【问题标题】:What's the proper approach to testing controllers in Laravel?在 Laravel 中测试控制器的正确方法是什么?
【发布时间】:2014-07-20 11:47:28
【问题描述】:

我正在重写现有的 Laravel 4 应用程序以确保有足够的测试。长话短说,我已经使用 TDD 方法重写了我的 AccountController 类,但我有点头疼。

考虑以下方法来呈现包含用户列表的页面:

public function getIndex()
{
    // build the view
    //
    return \View::make('account.list-users')
        ->with('users', \Sentry::getUserProvider()->findAll());
}

我正在使用 Smarty 呈现我的视图并使用 Sentry 进行身份验证。

现在,我想写一些这样的测试:

public function test_getIndex()
{
    // arrange
    //

    // set up some mocks here...


    // act
    //
    $response = $this->client->request("GET", "/list-users");


    // assert
    //

    // test for <table class="table">
    $this->assertFalse($response->filter("table.table")==null, "table not found");

    // test for some <a> tags for the "update" buttons
    $element = $response->filter("td a")->first()->extract(array("href", "class", "_text"));
    $this->assertTrue(strstr($element[0][0],"/my-update-url")!="");
    $this->assertTrue(strstr($element[0][1],"btn btn-xs btn-success")!="");
    $this->assertTrue(strstr($element[0][2],"Active")!="");

    // test for some other markup...

}

我一直在关注 Jeffrey Way 的书Laravel 测试解码,并编写了类似上述的测试,它们运行良好。

头痛出现在“在这里设置一些模拟......”部分。具体来说,我需要设置的模拟数量是荒谬的。这是因为,作为大型 Web 应用程序的一部分,我正在使用 View Composer,它向 View 模型添加数据:当前用户模型、菜单结构、警报消息、新闻消息、应用程序版本号等。 '已经通过使用“基本”模板进行测试来减少大部分内容,但它仍然有很多东西 - 以至于我编写了数百行代码来测试这个简单的单行方法。

有更好的方法吗?

在我看来,有两种方法:

A.我一直这样做的方式

B.模拟\View::make 调用,这样我的所有模板渲染都被绕过了——像这样

public function test_getIndex()
{
    // arrange
    //
    $userList = "this is a list of users";

    $userProvider = Mockery::mock("\Cartalyst\Sentry\Users\Eloquent\Provider");

    \Sentry::shouldReceive("getUserProvider")
        ->once()
        ->andReturn($userProvider);

    $userProvider->shouldReceive("findAll")
        ->once()
        ->andReturn($userList);

    $view = Mockery::mock("\Illuminate\View\View");

    \View::shouldReceive("make")
        ->with("account.list-users")
        ->once()
        ->andReturn($view);

    $view->shouldReceive("with")
        ->with("users", $userList)
        ->once()
        ->andReturn($view);

    $view->shouldReceive("render")
        ->once()
        ->andReturn("results");

    // act
    //
    $response = $this->call("GET", "/list-users");

    // assert
    //
    $this->assertResponseOk();
}

如果我采用这种方法,测试会简单得多,而且我只测试实际在控制器方法中的代码,但我并没有真正测试调用该路由所涉及的所有内容(这可能是件好事或者可能不会 - 我不确定)而且我担心我无法获得足够的覆盖范围。

那么,最好的方法是什么:(A)、(B) 还是其他?

编辑

对于我的控制器方法的测试,我有相当多的困惑,@TheShiftExchange 的回答和下面的 cmets 更清楚地说明了这一点。我将尝试在这里解决这个问题,作为一个编辑,因为它给了我更多的空间来讨论这个问题。

考虑下面答案中给出的第二个例子:

public function testMethod()
{
    $this->call('GET', '/list-users');

    $this->assertViewHas('users', \Sentry::getUserProvider()->findAll());
}

如果我运行这个测试,它会工作,但它会访问数据库,我试图通过模拟一些东西来避免这种情况。

所以,我可以稍微扩展一下这个测试:

public function testMethod()
{
    \Sentry::shouldReceive("getUserProvider")
        ->once()
        ->andReturn($userProvider);
    // plus a mock of the UserProvider class,...


    $this->call('GET', '/list-users');

    $this->assertViewHas('users', \Sentry::getUserProvider()->findAll());
}

这个测试将工作,因为除了控制器方法所需的模拟之外,我还需要对我的视图作曲家中的代码进行模拟。此代码包括$currentUser = \Sentry::getUser()(用户名显示在我的应用程序页面的右上角)等。

所以代码实际上变成了:

public function testMethod()
{
    \Sentry::shouldReceive("getUserProvider")
        ->once()
        ->andReturn($userProvider);
    // plus a mock of the UserProvider class,...

    // plus a mock of ThisClass

    // and a mock of ThatClass

    // and a mock of SomeOtherClass

    // etc.

    // etc.

    $this->call('GET', '/list-users');

    $this->assertViewHas('users', \Sentry::getUserProvider()->findAll());
}

而且很快就会失控。

这表明我做错了什么,但我不确定是什么。我怀疑问题源于我不确定我在这里测试的究竟是什么。

所以,毕竟,问题变成了这样:

当我测试控制器的方法时,我真正想要测试的是什么?

  1. 控制器方法中的代码?或者,

  2. 从请求到响应的全过程?

我要测试的是第一项 - 只是控制器方法中的代码。我的问题中的示例非常简单,但我确实有一些控制器方法可以根据用户输入执行表单验证或重定向等操作 - 我想测试该代码。

也许,与其通过$this-&gt;call() 测试代码,我需要直接调用控制器方法?

【问题讨论】:

    标签: php unit-testing laravel mockery


    【解决方案1】:

    作为 Laravel 框架的一部分,it includes some testing helpers。在这些帮助器中包括视图测试帮助器:

    断言视图有一些数据

    public function testMethod()
    {
        $this->call('GET', '/');
    
        $this->assertViewHas('name');
        $this->assertViewHas('age', $value);
    }
    

    所以你可以这样做:

    public function testMethod()
    {
        \Sentry::shouldReceive("getUserProvider")
        ->once()
        ->andReturn('foo');
    
        $this->call('GET', '/');
    
        $this->assertViewHas('users', 'foo');
    }
    

    【讨论】:

    • 是的,我知道这些断言,但我的问题更多是关于测试的设置,而不是最后的断言。
    • 我不明白 - 什么你想测试?如果视图获得了用户——那么你还需要测试吗?好像我已经用两行代码而不是 40 行代码测试了你的控制器功能?
    • 感谢您的 cmets - 您帮助我对问题进行了更多思考,我已经编辑了问题以(我希望)澄清它。
    • 我认为您试图在一个函数中测试太多。您的控制器 test 只是控制器响应正确的 route 并使用正确的 data 调用正确的 view .而已。它应该测试视图本身。同时,只需为您的视图编写器编写另一个单独的测试 - 它调用所有正确的变量。
    • 好的,我明白了。这是有道理的——我一直在嘲笑一些东西,因为我试图避免数据库访问(因为我认为这很重要)——这在这些测试中不重要吗?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-12-10
    • 1970-01-01
    • 1970-01-01
    • 2011-08-23
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多