Skip to content

Latest commit

 

History

History
225 lines (188 loc) · 7.58 KB

example-application-with-mezzio-testing.md

File metadata and controls

225 lines (188 loc) · 7.58 KB
title date draft toc images tags series
Example Application with Mezzio: Testing
2019-12-01 14:28:49 -0500
true
false
php
mezzio
laminas
prophecy
phpunit
Example Application with Mezzio

Where do we start?

I think it's a good exercise to start any application with a solid structure for testing the application with automated testing tools. That's why the first post in this series is about testing.

In this post, I'll show a basic setup for testing Mezzio applications. We'll get to some more advanced testing topics in later posts.

Legacy ZF Testing Tools

Those experienced with Zend Framework MVC applications are likely familiar with the zend-test library for ZF3, or Zend\Test for ZF2 and maybe even Zend_Test, the old ZF1 testing framework.

The documentation for these components allude to unit testing your application using these tools. That, unfortunately is a misnomer. All of the testing libraries provided by Zend Framework support integration testing or functional testing your code, but certainly not unit testing. The difference between unit tests and integration tests is dramatic. Those differences have been blogged about at length. Check the goog for more information about the different types of tests.

A Basic Example

Let's start with an example functional test of your new complete Mezzio application middleware stack. This example should be somewhat familiar if you're used to how zend-test works with old MVC apps. We'll test the ping route that comes with the Mezzio Skeleton Application.

You may have noticed that skeleton comes with some basic tests of the simple handlers that come with the skeleton. We'll show those in a later post. For now, we're going to start with a functional test of the GET /api/ping REST endpoint from end to end. This test will exercise the entire stack so we can prove that the endpoint "works".

I've created a couple of simple abstract classes that all of my functional tests can extend, making writing new tests fast and easy. These tests use real instances of your application components -- the dependency injection container and the application itself including the middleware pipeline and the routes. In fact the base class might remind you of an application bootstrap like that found in public/index.php in a Mezzio skeleton app.

Here's the base class for all functional tests. You'll note that this does nothing except initialize the application to be tested. Some assumptions must hold true for this to work out of the box. In short, if you start with the Mezzio skeleton, you should be all set. If the paths to your container/pipeline/routes config is non-standard, you might have to do some extra work -- perhaps a symlink or two.

<?php

declare(strict_types=1);

namespace FunctionalTest;

use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;
use Zend\Expressive\Application;
use Zend\Expressive\MiddlewareFactory;
use Helmich\Psr7Assert\Psr7Assertions;

/**
 * {@inheritdoc}
 */
abstract class AbstractFunctionalTest extends TestCase
{
    use Psr7Assertions;

    /** @var ContainerInterface */
    protected static $container;

    /** @var Application */
    protected static $app;

    public static function setUpBeforeClass(): void
    {
        static::initContainer();
        static::initApp();
        static::initPipeline();
        static::initRoutes();
    }

    public static function tearDownAfterClass(): void
    {
        static::$container = null;
        static::$app = null;
    }

    /**
     * Initialize new container instance.
     */
    protected static function initContainer(): void
    {
        static::$container = require 'config/container.php';
    }

    /**
     * Initialize app.
     */
    protected static function initApp(): void
    {
        static::$app = static::$container->get(Application::class);
    }

    /**
     * Initialize pipeline.
     */
    protected static function initPipeline(): void
    {
        (require 'config/pipeline.php')(
            static::$app,
            static::$container->get(MiddlewareFactory::class),
            static::$container
        );
    }

    /**
     * Initialize routes.
     */
    protected static function initRoutes(): void
    {
        (require 'config/routes.php')(
            static::$app,
            static::$container->get(MiddlewareFactory::class),
            static::$container
        );
    }
}

Any simple tests that require only a minimally bootstrapped application can extend the AbstractFunctionalTest class and test away.

Now it gets a little bit more interesting. Here's an abstract extension that adds some framework for defining REST endpoints to be tested and assertions for testing those endpoints:

<?php

declare(strict_types=1);

namespace FunctionalTest;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Stream;
use Zend\Diactoros\Request;
use Zend\Diactoros\Response;
use Zend\Diactoros\ServerRequest;
use Zend\Diactoros\Uri;
use PHPUnit\Framework\Constraint;

/**
 * Abstract to set up functional testing via endpoint provider config.
 */
abstract class AbstractFunctionalEndpointTest extends AbstractFunctionalTest
{
    /**
     * Provider for testEndpoint() method.
     *
     * @see self::testEndpoint() for provider signature
     *
     * @return array
     */
    abstract public function endpointProvider(): array;

    /**
     * @param string $method
     * @param string $uri
     * @param array  $requestHeaders
     * @param array  $body
     * @param array  $queryParams
     *
     * @return ServerRequestInterface
     */
    protected function getRequest(
        string $method,
        string $uri,
        array $requestHeaders = [],
        array $body = [],
        array $queryParams = []
    ): ServerRequestInterface {
        $uri = new Uri($uri);

        if (null !== $body) {
            $bodyStream = fopen('php://memory', 'r+');
            fwrite($bodyStream, json_encode($body));
            $body = new Stream($bodyStream);
        }

        if (!empty($queryParams)) {
            $uri = $uri->withQuery(http_build_query($queryParams));
        }

        return new ServerRequest(
            [],
            [],
            $uri,
            $method,
            $body ?? 'php://input',
            $requestHeaders ?? []
        );
    }

    /**
     * @dataProvider endpointProvider
     *
     * @param ServerRequestInterface $request
     * @param Constraint[] $responseConstraints
     */
    public function testEndpoint(
        ServerRequestInterface $request,
        array $responseConstraints = []
    ): void {
        $response = static::$app->handle($request);
        $this->assertInstanceOf(ResponseInterface::class, $response);
        $this->assertResponseConstraints($responseConstraints, $response);
    }

    /**
     * @param Constraint[]      $responseConstraints
     * @param ResponseInterface $response
     */
    protected function assertResponseConstraints(
        array $responseConstraints,
        ResponseInterface $response
    ): void {
        foreach ($responseConstraints as $msg => $constraint) {
            $this->assertThat(
                $response,
                $constraint,
                is_string($msg) ? $msg : ''
            );
        }
    }
}