Integration testing with slim

Posted on

Slim is very simple framework which is suitable for small projects http://www.slimframework.com/. I'm interested in this framework because it provides Request and Response which implements PSR-7 standards.

My case was: Create small API with slim.

The very important thing for me was to test the overall behavior of application, because of of possibility of regression.. There was not much examples about integration testing with slim but I created my own solution which I present below.

Example of application:

<?php

require __DIR__ . '/../vendor/autoload.php';

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

$app = new \Slim\App;

//List of posts
$app->get('/posts', function (ServerRequestInterface $request, ResponseInterface $response) {
    $posts = [
        [
            'title' => 'Example post 1',
            'content' => 'Aliquam erat volutpat.',
        ],
        [
            'title' => 'Example post 2',
            'content' => 'Vestibulum suscipit nulla quis orci.',
        ],
        [
            'title' => 'Example post 3',
            'content' => 'Phasellus magna.',
        ],
        [
            'title' => 'Example post 4',
            'content' => 'Sed augue ipsum, egestas nec.',
        ]
    ];

    $query = $request->getQueryParams();

    if (isset($query['page']) && $query['page'] >= 1) {
        $page = $query['page'];

        $postsPerPage = 2;

        $offset = ($page-1) * $postsPerPage;
        $length = $postsPerPage;

        $posts = array_slice($posts, $offset, $length);
    }

    $responseBody = $response->getBody();
    $responseBody->write(json_encode($posts));

    return $response
        ->withHeader('Content-Type', 'application/json')
        ->withStatus(200)
        ->withBody($responseBody);
});

//Add new post
$app->put('/posts/{postId}', function (ServerRequestInterface $request, ResponseInterface $response, $args = []) {
    $postId = $args['postId'];

    $responseBody = $response->getBody();
    $responseBody->write(json_encode(['id' => $postId]));

    return $response
        ->withHeader('Content-Type', 'application/json')
        ->withStatus(201)
        ->withBody($responseBody);
});

return $app;

It's a quite simple blog. Actions of this app are mocked - it's only the example.
There are routes:

  • PUT (add new post),
  • GET (list of post with ability to paginate it).

Now, I want to ensure that logic of adding and listing posts is integrated with slim correctly:

<?php

namespace Lzakrzewski\tests;

class BlogTest extends ApiTestCase
{
    /** @test */
    public function it_adds_new_post()
    {
        $this->request('PUT', '/posts/1', ['title' => 'A new blog post', 'content' => 'Hello world']);

        $this->assertThatResponseHasStatus(201);
        $this->assertThatResponseHasContentType('application/json');
        $this->assertArrayHasKey('id', $this->responseData());
    }

    /** @test */
    public function it_has_list_of_posts()
    {
        $this->request('GET', '/posts');

        $this->assertThatResponseHasStatus(200);
        $this->assertThatResponseHasContentType('application/json');
        $this->assertCount(4, $this->responseData());
    }

    /** @test */
    public function it_paginates_list_of_posts()
    {
        $this->request('GET', '/posts?page=2');

        $this->assertThatResponseHasStatus(200);
        $this->assertThatResponseHasContentType('application/json');
        $this->assertCount(2, $this->responseData());
    }
}

I expect status 201 on creation and status 200 on listing. I have also asserted count of list. In any case I don't need to response with status 500.

I created base class ApiTestCase for BlogTest (I was inspired by WebTestCase from symfony framework):

<?php

namespace Lzakrzewski\tests;

use Slim\App;
use Slim\Http\Environment;
use Slim\Http\Headers;
use Slim\Http\Request;
use Slim\Http\RequestBody;
use Slim\Http\Response;
use Slim\Http\Uri;

abstract class ApiTestCase extends \PHPUnit_Framework_TestCase
{
    /** @var Response */
    private $response;
    /** @var App */
    private $app;

    protected function request($method, $url, array $requestParameters = [])
    {
        $request = $this->prepareRequest($method, $url, $requestParameters);
        $response = new Response();

        $app = $this->app;
        $this->response = $app($request, $response);
    }

    protected function assertThatResponseHasStatus($expectedStatus)
    {
        $this->assertEquals($expectedStatus, $this->response->getStatusCode());
    }

    protected function assertThatResponseHasContentType($expectedContentType)
    {
        $this->assertContains($expectedContentType, $this->response->getHeader('Content-Type'));
    }

    protected function responseData()
    {
        return json_decode((string) $this->response->getBody(), true);
    }

    /** {@inheritdoc} */
    protected function setUp()
    {
        $this->app =  require __DIR__.'/../src/app.php';
    }

    /** {@inheritdoc} */
    protected function tearDown()
    {
        $this->app = null;
        $this->response = null;
    }

    private function prepareRequest($method, $url, array $requestParameters)
    {
        $env = Environment::mock([
            'SCRIPT_NAME' => '/index.php',
            'REQUEST_URI' => $url,
            'REQUEST_METHOD' => $method,
        ]);

        $parts = explode('?', $url);

        if (isset($parts[1])) {
            $env['QUERY_STRING'] = $parts[1];
        }

        $uri = Uri::createFromEnvironment($env);
        $headers = Headers::createFromEnvironment($env);
        $cookies = [];

        $serverParams = $env->all();

        $body = new RequestBody();
        $body->write(json_encode($requestParameters));

        $request = new Request($method, $uri, $headers, $cookies, $serverParams, $body);

        return $request->withHeader('Content-Type', 'application/json');
    }
}

There are methods to create request and assertions. The only setup is require file app.php from src/. Every request invokes application. Notice that during CLI testing Environment should be mocked.

Full working example is available in the link below:
https://github.com/lzakrzewski/slim-integration-testing-example