[英]TDD: best practices mocking stacks of objects
我正在尝试熟悉 PHP 中的单元测试和 Lumen 中的小 API。 在一些教程的帮助下编写前几个测试非常好,但现在我遇到了必须模拟/存根依赖项的问题。
我的 controller 取决于构造函数中提示的特定自定义接口类型。
当然,我在 ServiceProvider 中定义了这个接口/实现绑定。
public function __construct(CustomValidatorContract $validator)
{
// App\Contracts\CustomValidatorContract
$this->validator = $validator;
}
public function resize(Request $request)
{
// Illuminate\Contracts\Validation\Validator
$validation = $this->validator->validate($request->all());
if ($validation->fails()) {
$response = array_merge(
$validation
->errors() // Illuminate\Support\MessageBag
->toArray(),
['error' => 'Invalid request data.']
);
// response is global helper
return response()->json($response, 400, ['Content-Type' => 'application/json']);
}
}
如您所见,我的CustomValidatorContract
有一个validate()
方法,它返回一个Illuminate\Contracts\Validation\Validator
实例(验证结果)。 当调用errors()
时,这又会返回Illuminate\Support\MessageBag
的实例。 MessageBag
然后有一个toArray()
方法。
现在我想测试我的 controller 的行为,以防验证失败。
/** @test */
public function failing_validation_returns_400()
{
$EmptyErrorMessageBag = $this->createMock(MessageBag::class);
$EmptyErrorMessageBag
->expects($this->any())
->method('toArray')
->willReturn(array());
/** @var ValidationResult&\PHPUnit\Framework\MockObject\MockObject $AlwaysFailsTrueValidationResult */
$AlwaysFailsTrueValidationResult = $this->createStub(ValidationResult::class);
$AlwaysFailsTrueValidationResult
->expects($this->atLeastOnce())
->method('fails')
->willReturn(true);
$AlwaysFailsTrueValidationResult
->expects($this->atLeastOnce())
->method('errors')
->willReturn($EmptyErrorMessageBag);
/** @var Validator&\PHPUnit\Framework\MockObject\MockObject $CustomValidatorAlwaysFailsTrue */
$CustomValidatorAlwaysFailsTrue = $this->createStub(Validator::class);
$CustomValidatorAlwaysFailsTrue
->expects($this->once())
->method('validate')
->willReturn($AlwaysFailsTrueValidationResult);
$controller = new ImageResizeController($CustomValidatorAlwaysFailsTrue);
$response = $controller->resize(new Request);
$this->assertEquals(400, $response->status());
$this->assertEquals(
'application/json',
$response->headers->get('Content-Type')
);
$this->assertJson($response->getContent());
$response = json_decode($response->getContent(), true);
$this->assertArrayHasKey('error', $response);
}
这是一个运行良好的测试 - 但是有人可以告诉我是否有更好的方法来编写它吗? 感觉不对。 由于我在后台使用框架,是否需要这么大堆 moc 对象? 还是我的架构有问题,以至于感觉如此“过度设计”?
谢谢
您所做的不是单元测试,因为您没有测试应用程序的单个单元。 这是一个集成测试,使用单元测试框架执行,这就是它看起来直观错误的原因。
单元测试和集成测试发生在不同的时间、不同的地方,并且需要不同的方法和工具——前者测试代码的每一个 class 和 function,而后者并不关心这些,他们只是请求 API 并验证响应。 此外,IT 并不意味着 mocking 任何事情,因为它的目标是测试您的单元之间的集成程度。
您将很难支持这样的测试,因为每次更改CustomValidatorContract
时,您都必须修复所有涉及它的测试。 这就是 UT 如何通过要求它尽可能松耦合来改进代码设计(因此您可以选择一个单元并使用它而无需启动整个应用程序),尊重SRP和OCP等。
您不需要测试第 3 方代码,而是选择一个已经测试过的代码。 您也不需要测试副作用,因为环境就像 3rd 方服务,应该单独测试( return response()
是副作用)。 它还严重减慢了测试速度。
所有这些导致您只想单独测试您的CustomValidatorContract
的想法。 你甚至不需要在那里模拟任何东西,只需实例化验证器,给它几组输入数据并检查它是如何进行的。
这是一个运行良好的测试 - 但是有人可以告诉我是否有更好的方法来编写它吗? 感觉不对。 由于我在后台使用框架,是否需要这么大堆 moc 对象? 还是我的架构有问题,以至于感觉如此“过度设计”?
大量的模拟对象表明您的测试对象与许多不同的事物紧密耦合。
如果您想支持更简单的测试,那么您需要使设计更简单。
换句话说,不是Controller.resize
是一个了解所有细节的巨大的整体事物,而是考虑一个 resize 只知道事物表面的设计,以及如何将工作委托给其他(更容易测试的)部分.
这是正常的,因为 TDD 主要是为了选择支持更好测试的设计。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.