我正在重写现有的 Laravel 4 应用程序以确保有足够的测试。长话短说,我已经AccountController
使用 TDD 方法重写了我的课程,但我有点头疼。
考虑以下呈现包含用户列表的页面的方法:
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 Testing Decoded andwritten tests like the one,它们运行良好。
头痛出现在“在这里设置一些模拟......”部分。具体来说,我需要设置的模拟数量是荒谬的。这是因为,作为一个更大的 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 的回答和下面的评论更清楚了。我将尝试在这里解决这个问题,作为一个编辑,因为它给了我更多讨论这个问题的空间。
考虑以下答案中给出的第二个示例:
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());
}
它很快就会失控。
这向我表明我做错了什么,但我不确定是什么。我怀疑这个问题源于我不确定我在这里测试的究竟是什么。
所以,毕竟,问题变成了这样:
当我测试控制器的方法时,我真正想要测试的是什么?
控制器方法中的代码?或者,
从请求到响应的全过程?
我要测试的是第一项 - 只是控制器方法中的代码。我的问题中的示例非常简单,但我确实有一些控制器方法可以根据用户输入执行表单验证或重定向等操作 - 我想测试该代码。
也许,$this->call()
我不需要通过 测试代码,而是直接调用控制器方法?