I'm trying to get familiar with unit testing in PHP with a small API in Lumen. Writing the first few tests was pretty nice with the help of some tutorials but now I encountered a point where I have to mock/ stub a dependency.
My controller depends on a specific custom interface type hinted in constructor.
Of course I defined this interface/implementation-binding within a 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']);
}
}
As you can see, my CustomValidatorContract
has a method validate()
which returns an instance of Illuminate\Contracts\Validation\Validator
(the validation result). This in turn returns an instance of Illuminate\Support\MessageBag
when errors()
is called. MessageBag
then has a toArray()
-method.
Now I want to test the behavior of my controller in case the validation fails.
/** @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);
}
Although this test is working after a day of research I'm pretty sure that I miss something pretty important here. None of the tutorials I've seen mentioned, that the annotation is necessary to make sure the object type of a mock is a specific one. It was eventually the only way for me to make this working. I also dispensed with creating actual double classes and tried to make them on the fly with built-in features. But I know that the possibilities are there.
I would really appreciate it if you could give me an advice if this is at least in the near to an expected test method.
Do I really have to write the annotation if I have to deal with a specific object type?
Is there still something wrong with my architecture so that this feels so "overengineered"?
OK, I was pretty sure that there was an error thrown by PHPUnit because of the wrong type delivered to the controller. Actually, this was something else.
These annotations are only necessary to satisfy Intelephense about the return-type of the getStub()
or getMock()
method.
The test is running without any error even without the annotation.