How to really do a unit test on a slim controller

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.

1 Like

Hi @margikern

Generally, I would try to avoid excessive mocking.

I’m the author of the Slim 4 - Testing article and I also provided an example for mocking Repositories, but this should be used only in rare cases.

Try to test and cover all the code you actually deploy.

The downside of mocking is that you are not testing and covering
the code that you actually deploy and run on your real system.
The phpunit code coverage can never be 100% when you mock your repositories.
and the test quality will never be as good as in real integration tests.

Another problem with mocking is that your tests become very large and complex,
and the test needs to know all the implementation details that could change
quickly during the next refactoring. Maintaining such mocked tests would
become more difficult and expensive in the long run.

Mocking makes sense if you have external APIs that must not be touched when
the test is running. So, for example, I would only mock HTTP requests for external APIs.

When you write tests for the Actions, better write integration tests that cover the Actions,
the Services, and the Repositories all at once. Then your tests are easier
to setup, and you cover all the code you actually deploy.

1 Like

thanks for the answer, I even saw a post on the internet by some people advising not to do unit testing on the controller, some for the reason of not being efficient, because there is not much logic to be tested on a controller, since the objective of the controller is to receive a request and pass the inputs to a model, and it would be more interesting to do an integration test or functional test.

I haven’t yet tackled this problem of excessive mocking, as I recently tested, so I can’t know how bad excessive mocks must be, the day I go through it, I believe I will have a bigger view of it.

Thank you for the reply and for your post, it gave me a great light, mainly on how to do an integration test with phpunit, because I imagined that it was only possible to do unit testing with it.