我正在尝试通过Lumen中的小API熟悉PHP中的单元测试。在一些教程的帮助下,编写前几个测试非常不错,但是现在我遇到了必须模拟/存根依赖项的问题。
我的控制器取决于构造函数中提示的特定自定义接口类型。当然,我在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
的实例(验证结果)。当调用Illuminate\Support\MessageBag
时,这又返回errors()
的实例。 MessageBag
然后具有toArray()
方法。
现在,我想测试控制器的行为,以防验证失败。
/** @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对象堆栈?还是我的体系结构有问题,以至于感觉“设计过度”?
谢谢
您正在做的不是单元测试,因为您不是在测试应用程序的单个单元。这是一个集成测试,是通过单元测试框架执行的,这就是直观上看起来错误的原因。
[单元测试和integration testing]发生在不同的时间,不同的位置,并且需要不同的方法和工具-前者测试代码的每个类和功能,而后者则不关心它们,它们只请求API和验证响应。另外,IT并不暗示要嘲笑任何东西,因为它的目的是测试您的单元之间的集成程度。
您将很难支持这样的测试,因为每次更改CustomValidatorContract
时,您都必须修复涉及该测试的所有测试。这就是UT通过要求其尽可能松散地耦合(因此您可以选择一个单元并使用它而无需启动整个应用程序)来改进代码设计的方式,同时遵守SRP和OCP等。
您不需要测试第三方代码,而是选择一个已经测试过的代码。您也不需要测试副作用,因为环境就像第三方服务一样,因此应该单独进行测试(return response()
是副作用)。同样,它也会严重减慢测试速度。
所有这些导致您只想单独测试CustomValidatorContract
的想法。您甚至不需要在此模拟任何东西,只需实例化验证器,为它提供几组输入数据并检查其运行情况。
这是可以正常运行的测试-但是有人可以告诉我是否有更好的编写方法吗?感觉不对。是否由于我在后台使用框架而需要大量的moc对象堆栈?还是我的体系结构有问题,以至于感觉“设计过度”?
大量的模拟对象表明您的测试主题与许多不同的事物紧密相关。
如果要支持更简单的测试,则需要使设计更简单。
换句话说,不是Controller.resize
是一堆巨大的整体事物,它了解所有事物的所有细节,而是考虑一种设计,其中调整大小仅了解事物的表面,以及如何将工作委托给其他人(更容易测试) )件。
这很正常,从某种意义上说,TDD在选择支持更好测试的设计上很有用。