Hello, I’m new to the slim framework and the testing world, I know there are already posts on the forum talking about tests on the controller, but I ended up with a doubt that is consuming me for 3 days. I am currently trying to do a unit test on the controller.
My controller action
// NewOrEditAction
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface
{
try {
$user = isset($args['id']) && ! empty($args['id'])
? $this->userRepository->getById($args['id'])
: new User();
$this->userInputFilter->setData($request->getParsedBody());
$this->userRepository->begin();
if ($this->userInputFilter->isValid() === false) {
return $this->responder->withJson(
$response,
['messages' => $this->userInputFilter->getMessages()],
400
);
}
$user = $this->hydrator->hydrate(
$request->getParsedBody(),
$user
);
$this->userRepository->persist($user);
$this->userRepository->commit();
return $this->responder->withJson(
$response,
['message' => 'Successfully saved!'],
200
);
} catch (\Exception $e) {
return $this->responder->withJson(
$response,
['message' => $e->getMessage()],
400
);
}
}
I found two ways to do tests with slim.
The first way would be using the container, seen in this tutorial Slim 4 - Testing | Daniel Opitz - Blog, where it is used of an AppTest::mock
method to mock the dependencies.
Example using method mock
:
/**
* @test
*/
public function addAnNewRegistryWithSuccess()
{
$post = [
'name' => 'Igor A.C',
'email' => 'igorac1999@teste.com',
'password' => 'igorac1999',
'passwordConfirm' => 'igorac1999'
];
$user = new User();
$user->setName('Igor A.C');
$user->setEmail('igorac1999@teste.com');
$user->setPassword('igorac1999');
$this->mock(UserInputFilter::class)
->expects($this->once())
->method('isValid')
->willReturn(true);
$this->mock(HydratorInterface::class)
->expects($this->once())
->method('hydrate')
->willReturn($user);
$this->mock(UserRepositoryInterface::class)
->expects($this->once())
->method('persist')
->with($user);
$request = $this->createJsonRequest(
'POST',
'/users/new-or-edit',
$post
);
$response = $this->app->handle($request);
$this->assertJsonData(['message' => 'Successfully saved!'], $response);
}
The second way would be to instantiate the controller and manually touch the dependencies, found in this tutorial Testing Slim Framework actions – Rob Allen's DevNotes
// makeMock() -> It's a custom method that I created, that practically returns a mock using the methods of phpunit itself
public function __construct()
{
parent::__construct();
$this->responder = $this->makeMock(Responder::class);
$this->userInputFilter = $this->makeMock(UserInputFilter::class);
$this->userRepository = $this->makeMock(UserRepositoryInterface::class);
$this->hydrator = $this->makeMock(HydratorInterface::class);
}
/**
* @test
*/
public function addAnNewRegistryWithSuccesss()
{
// given
$post = [
'name' => 'New Name',
'email' => 'igorac1999@teste.com',
'password' => 'igorac1999',
'passwordConfirm' => 'igorac1999'
];
// when
$request = $this->createJsonRequest(
'POST',
'/users/new-or-edit',
$post
);
$user = new User();
$user->setName('Igor A.C');
$user->setEmail('igorac1999@teste.com');
$user->setPassword('igorac1999');
$this->userInputFilter
->expects($this->once())
->method('isValid')
->willReturn(true);
$this->hydrator
->expects($this->once())
->method('hydrate')
->with(
$post,
new User()
)
->will($this->returnValue($user));
$this->userRepository
->expects($this->once())
->method('persist')
->with($user);
$this->responder
->method('withJson')
->willReturn($this->createJsonResponse(200, ['message' => 'Successfully saved!']));
$newOrEditAction = new NewOrEditAction(
$this->responder,
$this->userInputFilter,
$this->userRepository,
$this->hydrator
);
$response = $newOrEditAction($request, new Response(), []);
// then
$this->assertJsonData(['message' => 'Successfully saved!'], $response);
}
public function makeMock(string $namespace): MockObject
{
if (! class_exists($namespace) && ! interface_exists($namespace)) {
throw new \InvalidArgumentException(sprintf('Class or Interface %s not exists', $namespace));
}
$mock = $this->getMockBuilder($namespace)
->disableOriginalConstructor()
->getMock();
return $mock;
}
The first is interesting because it makes it easier to mock the dependencies, but sometimes it is not possible because I have dependencies that cannot be solved automatically by the container, so in this case I need to use the 2nd option.
Both ways work, but in the second way I was a little confused about testing a controller, as it is necessary to mock the responder::withJson()
so that I can apply an assertion, and this seems a little weird or I don’t really know how to test a controller unit or what to test on a controller.
If anyone can set an example based on this one, I will be grateful.