Pragmatism in the real world

Testing Slim Framework actions

To test a Slim Framework action, you need a request and a response object and mock whatever is in the action. This is one way to do this.

Consider this simple echo action that returns the query parameters to you as a JSON encoded string:

$ curl "http://localhost:8888/echo?foo=bar&this=that"
{"foo":"bar","this":"that"}

This is one of those useful API endpoints that your users can use to check that everything is working as expected.

The action under test

The code for this endpoint is a class called EchoAction which looks like this:

namespace App\Action;

use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;

class EchoAction
{
    public function __invoke(Request $request, Response $response, $args = [])
    {
        return $response->withJson($request->getQueryParams());
    }
}

and it is registered with the Slim App like so:

$app->get('/echo', App\Action\EchoAction::class);

Testing

Testing it isn’t too complex as there are no dependencies on the EchoAction class itself, so we just have instantiate the class, invoke it and write an test.

For a URL of the form /echo?foo=bar, the core test code is:

$action = new \App\Action\EchoAction();
$response = $action($request, $response);
$this->assertSame((string)$response->getBody(), json_encode(['foo' => 'bar']));

Creating the request and response

Creating the $response is easy as the constructor parameters all have defaults:

$response = new \Slim\Http\Response();

The $request is a little more complex as it’s constructor signature looks like this:

public function __construct(
        $method,
        UriInterface $uri,
        HeadersInterface $headers,
        array $cookies,
        array $serverParams,
        StreamInterface $body,
        array $uploadedFiles = []
    )

However, Slim actually creates a Slim\Http\Request using the static createFromEnvironment() factory method, which takes a Slim\Http\Environment instance. Roughly, it does this:

$request = Request::createFromEnvironment(new Environment($_SERVER));

Setting up a $_SERVER array with the relevant elements can be a little tiresome in testing though. Fortunately, Josh needed to test Slim itself, so the Environment object has a handy static method called mock() that does this for us.

We use it like this:

$environment = \Slim\Http\Environment::mock([
    'REQUEST_METHOD' => 'GET',
    'REQUEST_URI' => '/echo',
    'QUERY_STRING'=>'foo=bar'
]);

As you can see, mock() takes an array which contains $_SERVER keys that we wish to set up for our particular test. Usually we set the REQUEST_METHOD, REQUEST_URI and, if we need it, QUERY_STRING. We need QUERY_STRING as this is the key in $_SERVER that Request uses to determine the query parameters for the request.

Putting it all together

Hence, our completed test looks like this:

class EchoActionTest extends \PHPUnit_Framework_TestCase
{
    public function testGetRequestReturnsEcho()
    {
        // instantiate action
        $action = new \App\Action\EchoAction();

        // We need a request and response object to invoke the action
        $environment = \Slim\Http\Environment::mock([
            'REQUEST_METHOD' => 'GET',
            'REQUEST_URI' => '/echo',
            'QUERY_STRING'=>'foo=bar']
        );
        $request = \Slim\Http\Request::createFromEnvironment($environment);
        $response = new \Slim\Http\Response();

        // run the controller action and test it
        $response = $action($request, $response, []);
        $this->assertSame((string)$response->getBody(), '{"foo":"bar"}');
    }
}

All in all, it’s not too complicated to test a Slim action and Environment::mock() makes it easy to set up particular test cases.

5 thoughts on “Testing Slim Framework actions

  1. Hi Rob, thank you for taking the time and writing about this subject. Nevertheless, I have a small doubt. What if my middleware, or my action class has a dependency? Since the application singleton has gone, how can I access the DI container in the test to inject the dependency, or dependencies?

    You instantiate EchoAction without problems in this case, but only because there are no dependencies. I suppose I could instantiate my dependency, and pass it to EchoAction, but what if my dependency has a dependency?

    I have registered everything in the container, now I need a way to access it in the test! Again, thanks for the hard work and dedication.

  2. Hi again Rob, I have another doubt. How about testing just one Middleware? I have 4 very simple middlewares, one of them being CorsMiddleware, and another one CsrfMiddleware.

    The signature is the common one: public function __invoke(Request $request, Response $response, $next). I have registered it in the DI container, and also added it to the stack with $app->add(\app\middleware\CorsMiddleware::class);

    The question is: if I don't wanna test the other two, and this is the first one being executed, how can I stop the $response = $next($request, $response) from being called? I mean, can I mock the next callable (CsrfMiddleware in this case) and set expectations about __invoke?

    BTW, thanks for the super quick response, I think you were bored or something.

Comments are closed.