code-01

How to test Controllers using PHPUnit

By Chris Taylor, published Wednesday, February 4th, 2015


This is a quick post to show you how I write and test Controllers, although it may not be to a correct standard, the testing principles can be applied to any project.

A Controller can be used to direct requests into the correct parts of your application. They can coordinate data from Repositories, Services and much more. In this example we’ll be adding a Controller to a Repository that I’ve made and tested in another blog post here. Like Repositories, Controllers should implement a standard interface, which declares certain functionality that all Controllers need to implement, and extend a base Controller that defines functionality that all Controllers have access to. In this this example the Controller will take a HTTP request with a JSON body and pass it into the correct Repository.

<?php

    namespace Controller;

    interface IController
    {
        // Here you can declare functions 
        // that all controllers would 
        // need to implement
    }

?>

<?php

    namespace Controller;

    abstract class BaseController implements IController 
    {
        // Here you can define functions 
        // that all controllers would 
        // have access to
    }

?>

<?php 

    namespace Controller;

    /**
     * Directs HTTP requests that are to do with Accounts into the AccountRepository
     */
    class AccountsController extends BaseController 
    {

        /**
         * @var  AccountRepository $repo
         */
        protected $repo;

        public function __construct(AccountRepository $repo)
        {

            $this->repo = $repo;

        }

        /**
         * Adds a login to an account
         *
         * @param string $id The account ID to add the login to
         * @param Request $request The HTTP request containing all the required information
         * @return JsonResponse The HTTP response to be sent to the user
         */
        public function addLoginAction($id, Request $request)
        {

            /*
             * Try to JSON decode the posted body
             */
            $payload = json_decode($request->getContent());

            /*
             * json_decode returns null if it isn't valid JSON
             */
            if(is_null($payload))
            {
                return new JsonResponse('Bad payload', 400);
            }

            /*
             * Now try to call the required action in the Repository
             */
            try 
            {

                $result = $this->repo->addLogin($id, $payload);

                /*
                 * Woo no error, return a success code 
                 */
                return new JsonResponse($result, 200);

            } catch (Exception $e) 
            {
                /*
                 * Opps something went wrong, return an error message  
                 */
                return new JsonResponse($e->getMessage(), 400);
            }

        }

    }

?>

As you can see in the code above, all the controller does is take the HTTP request and put the correct data in the Repository. Regardless of this, there is complexity in the controller and therefore needs to be tested. We can test the following things:

  1. The function correctly decodes the JSON body and checks if it’s valid JSON.
  2. The function calls the correct function of the Repository with the correct data, and:
    1. Returns a success code if no exception was thrown
    2. Returns an error code if an exception was thrown and caught

Below is an example of a class that would test such things:

<?php

class AccountsControllerTest extends WebTestCase
{

    /**
     * This sets up the application to be used in the test suite
     */
    public function createApplication()
    {
        $this->app = require __DIR__.'/../web/index_dev.php';
        return $this->app;
    }   

    /*
     * This sets up a mock of the AccountRepository to be used in the tests
     */
    public function setUp()
    {

        $this->app['account.repository'] = $this->getMockBuilder('\Repository\AccountRepository')
            ->disableOriginalConstructor()->getMock();

    }

    public function testAddLoginActionChecksForValidJSON()
    {

        /*
         * This mocks out a HTTP request so we can get it to 
         * return what we want 
         */
        $mockRequest = $this->getMockBuilder('\Symfony\Component\HttpFoundation\Request')
        ->disableOriginalConstructor()->getMock();

    $mockRequest->method('getContent')->will($this->returnValue('Something that isn\'t valid JSON'));

    /*
     * This tells our test that we don't want this function 
     * to be called at all in the test
     */
    $this->app['account.repository']
        ->expects($this->never())
        ->method('addLogin');

    $controller = new Controller\AccountsController($this->app['account.repository']);

    $response = $controller->addLoginAction(12323123123123, $mockRequest);

    /*
     * First we check that we've got a JsonResponse back 
     * from the controller, then that we've got the correct status code,
     * and finally that we got the correct content back 
     */
    $this->assertInstanceOf('\Symfony\Component\HttpFoundation\JsonResponse', $response);
    $this->assertEquals(400, $response->getStatusCode());
    $this->assertEquals('"Bad payload"', $response->getContent());

    }

    public function testAddLoginActionReturnsErrorCode()
    {

        $mockRequest = $this->getMockBuilder('\Symfony\Component\HttpFoundation\Request')
        ->disableOriginalConstructor()->getMock();

    $mockRequest->method('getContent')->will($this->returnValue('["Something that is valid JSON"]'));

    /*
     * This tells the test that we want the function 
     * to throw an exception
     */
    $this->app['account.repository']
        ->expects($this->once())
        ->method('addLogin')
        ->with(["Something that is valid JSON"])
        ->will($this->throwException(new Exception('An error message')));

    $controller = new Controller\AccountsController($this->app['account.repository']);

    $response = $controller->addLoginAction(12323123123123, $mockRequest);

    /*
     * First we check that we've got a JsonResponse back 
     * from the controller, then that we've got the correct status code,
     * and finally that we got the correct content back 
     */
    $this->assertInstanceOf('\Symfony\Component\HttpFoundation\JsonResponse', $response);
    $this->assertEquals(400, $response->getStatusCode());
    $this->assertEquals('"An error message"', $response->getContent());

    }

    public function testAddLoginActionReturnsSuccessCode()
    {

        $mockRequest = $this->getMockBuilder('\Symfony\Component\HttpFoundation\Request')
        ->disableOriginalConstructor()->getMock();

    $mockRequest->method('getContent')->will($this->returnValue('["Something that is valid JSON"]'));

    $this->app['account.repository']
        ->expects($this->once())
        ->method('addLogin')
        ->with(["Something that is valid JSON"])
        ->will($this->returnValue('A success message'));

    $controller = new Controller\AccountsController($this->app['account.repository']);

    $response = $controller->addLoginAction(12323123123123, $mockRequest);

    /*
     * First we check that we've got a JsonResponse back 
     * from the controller, then that we've got the correct status code,
     * and finally that we got the correct content back 
     */
    $this->assertInstanceOf('\Symfony\Component\HttpFoundation\JsonResponse', $response);
    $this->assertEquals(200, $response->getStatusCode());
    $this->assertEquals('"A success message"', $response->getContent());

    }

It’s important to test the business logic of your application, as errors here can typically be the most serious and difficult to trace the origin of. If you’re integrating with a continuous deployment service, you don’t even have to worry about running the tests yourself.

References


The PHP micro-framework based on the Symfony2 Components – http://silex.sensiolabs.org/doc/testing.html