我在我的项目中使用 Laravel,我是单元/功能测试的新手,所以我想知道在编写测试时处理更复杂的功能用例的最佳方法是什么?
让我们来看这个测试例子:
// tests/Feature/UserConnectionsTest.php
public function testSucceedIfConnectAuthorised()
{
$connection = factory(Connection::class)->make([
'sender_id' => 1,
'receiver_id' => 2,
'accepted' => false,
'connection_id' => 5,
]);
$user = factory(User::class)->make([
'id' => 1,
]);
$response = $this->actingAs($user)->post(
'/app/connection-request/accept',
[
'accept' => true,
'request_id' => $connection->id,
]
);
$response->assertLocation('/')->assertStatus(200);
}
所以我们遇到了这种情况,我们在两个用户之间有一些连接系统。其中一个用户创建的数据库中有一个 Connection
条目。现在要使它成为一个成功的连接,第二个用户必须批准它。问题出在 UserController
通过 connectionRequest
接受这个:
// app/Http/Controllers/Frontend/UserController.php
public function connectionRequest(Request $request)
{
// we check if the user isn't trying to accept the connection
// that he initiated himself
$connection = $this->repository->GetConnectionById($request->get('request_id'));
$receiver_id = $connection->receiver_id;
$current_user_id = auth()->user()->id;
if ($receiver_id !== $current_user_id) {
abort(403);
}
[...]
}
// app/Http/Repositories/Frontend/UserRepository.php
public function GetConnectionById($id)
{
return Connection::where('id', $id)->first();
}
所以我们在测试函数中得到了这个假的(工厂创建的)连接,然后我们不幸地使用它的假 ID 在真实连接中的真实数据库中运行检查,这不是我们想要的:(
通过研究,我发现了创建接口(interface)的想法,这样我们就可以根据是否进行测试来提供不同的方法体。就像这里的 GetConnectionById()
一样,可以轻松伪造测试用例的答案。这似乎没问题,但是:
- 对于一个人来说,这看起来像是一种开销,除了编写测试之外,我还必须使“真实”代码本身更加复杂,以达到测试的唯一目的。
- 第二件事,我阅读了 Laravel 文档中关于测试的所有内容,但没有一处提到使用接口(interface),所以这也让我想知道这是否是解决此问题的唯一方法和最佳方法问题。
最佳答案
我会尽力帮助您,当有人开始测试时,这一点都不容易,特别是如果您没有强大的框架(甚至根本没有框架)。
所以,让我试着帮助你:
- 区分单元测试和功能测试非常重要。您正确地使用了功能测试,因为您想测试业务逻辑而不是直接测试类。
- 测试时,我个人的建议是始终创建第二个数据库以仅用于测试。它必须始终完全是空的。
- 因此,要实现此目的,您必须在
phpunit.xml
中定义正确的环境变量,这样当您只运行测试时,您不必施展魔法就可以让它工作。 - 另外,使用
RefreshDatabase
特征。因此,每次您运行测试时,它都会删除所有内容,再次迁移您的表并运行测试。
- 因此,要实现此目的,您必须在
- 您应该始终创建测试运行所必需的内容。例如,如果您要测试用户是否可以取消他/她创建的订单,您只需要有一个
product
、一个user
和一个invoice
与product
和user
关联。您不需要创建notifications
或与此无关的任何内容。您必须拥有您期望在真实案例场景中拥有的东西,但没有多余的东西,这样您就可以真正地测试它是否完全适用于最少的东西。 - 你可以run seeders如果你的设置是“大”,那么你应该使用
setup
方法。 - 请记住永远不要模拟核心代码,例如
request
或controllers
或任何类似的东西。如果你 mock 其中任何一个,你就做错了什么。 (一旦您真正知道如何测试,您将通过经验学习这一点)。 - 在编写测试名称时,切记不要使用
if
和must
等类似的措辞,而是使用when
和should
。例如,您的测试testSucceedIfConnectAuthorised
应命名为testShouldSucceedWhenConnectAuthorised
。 - 这个提示非常个人化:不要在 Laravel 中使用
RepositoryPattern
,它是一种反模式。这不是最糟糕的使用方式,但我建议使用Service
类(不要与Service Provider
混淆,我指的是普通类,它是仍然调用Service
) 来实现你想要的。但是,你仍然可以用谷歌搜索这个和 Laravel,你会看到每个人都不鼓励 Laravel 中的这种模式。 - 最后一个提示,
Connection::where('id', $id)->first()
与Connection::find($id)
完全相同>. - 我忘了补充一点,您应该始终对您的
URL
进行硬编码(就像您在测试中所做的那样),因为如果您依赖于route('url.name')
并且名称匹配但真正的URL
是/api/asdasdasd
,您永远不会测试该 URL 是否是您想要的。恭喜你!很多人不这样做,那是错误的。
因此,为了帮助您处理您的情况,我假设您有一个清晰的数据库(没有表的数据库,RefreshDatabase
特性将为您处理)。
我会像这样进行您的第一次测试:
public function testShouldSucceedWhenConnectAuthorised()
{
/**
* I have no idea how your relations are, but I hope
* you get the main idea with this. Just create what
* you should expect to have when you have this
* test case
*/
$connection = factory(Connection::class)->create([
'sender_id' => factory(Sender::class)->create()->id,
'receiver_id' => factory(Reciever::class)->create()->id,
'accepted' => false,
'connection_id' => factory(Connection::class)->create()->id,
]);
$response = $this->actingAs(factory(User::class)->create())
->post(
'/app/connection-request/accept',
[
'accept' => true,
'request_id' => $connection->id
]
);
$response->assertLocation('/')
->assertOk();
}
然后,除了指向您的测试数据库(本地)的 phpunit.xml
环境变量外,您不应该更改任何内容,并且它应该在您不更改代码中的任何内容的情况下工作。
关于laravel - 如何使用 PHPUnit 在 Laravel 上测试更复杂的案例,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/69150653/