Mezzio Example: Functional and Unit Testing


Where do we start?

It’s a good choice 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. Any well-tested application will typically have more lines of testing code than actual application code. Starting with a good structure for testing will pay dividends down the road.

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 Laminas MVC (FKA Zend Framework MVC) applications are likely familiar with the laminas-test library, or Zend\Test for ZF v2 and maybe even Zend_Test, the old ZF v1 testing framework.

The documentation for these components allude to unit testing your application using these tools. That, unfortunately is a misnomer. The testing component provided by Laminas is geared for integration testing or functional testing your MVC stack, but not really 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 Functional Test

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 laminas-test works with 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 is functional under the conditions we’ve set up in the test.

I’ve created a simple abstract class 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.

Abstract for all Functional Tests

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<?php
declare(strict_types=1);

namespace AppFunctionalTest;

use Helmich\JsonAssert\Constraint\JsonValueMatchesMany;
use Helmich\Psr7Assert\Psr7Assertions;
use Mezzio\Application;
use Mezzio\MiddlewareFactory;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\Constraint\Constraint;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;

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

    protected ContainerInterface $container;

    protected function setUp(): void
    {
        parent::setUp();
        $this->initContainer();
        $this->initApp();
        $this->initPipeline();
        $this->initRoutes();
    }

    protected function initContainer(): void
    {
        $this->container = require __DIR__ . '/../../config/container.php';
    }

    protected function initApp(): void
    {
        $this->app = $this->container->get(Application::class);
    }

    protected function initPipeline(): void
    {
        $factory = $this->container->get(MiddlewareFactory::class);
        (require __DIR__ . '/../../config/pipeline.php')($this->app, $factory, $this->container);
    }

    protected function initRoutes(): void
    {
        $factory = $this->container->get(MiddlewareFactory::class);
        (require __DIR__ . '/../../config/routes.php')($this->app, $factory, $this->container);
    }

    /**
     * Override parent method's hard-coded regex
     */
    public static function bodyMatchesJson(array $constraints): Constraint
    {
        return Assert::logicalAnd(
            self::hasHeader(
                'content-type',
                Assert::matchesRegularExpression(
                    ',^application/(.+\+)?json(;.+)?$,'
                )
            ),
            self::bodyMatches(
                Assert::logicalAnd(
                    Assert::isJson(),
                    new JsonValueMatchesMany($constraints)
                )
            )
        );
    }
}

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 official 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.

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

Let’s define a test.

Functional Test Ping Handler

Here’s a simple implementation of a functional endpoint test for GET /api/ping.

Note this extends the above AppFunctionalTest\AbstractFunctionalTest.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php

declare(strict_types=1);

namespace AppFunctionalTest;

use Laminas\Diactoros\ServerRequest;
use PHPUnit\Framework\Assert;

/**
 * Tests only PING.
 */
class PingTest extends AbstractFunctionalTest
{
    public function testPing(): void
    {
        $request = new ServerRequest(
            uri: '/api/ping',
            method: 'GET'
        );

        $response = $this->app->handle($request);
        self::assertThat($response, self::isSuccess());
        self::assertThat($response, self::bodyMatchesJson([
            'ack' => Assert::greaterThanOrEqual(time()),
        ]));
    }
}

The result of the above example is a functional test of the GET /api/ping route, asserting simply that we receive the expected response body.

A Basic Example Unit Test

Now we’re going to establish full test coverage on the App\Handler\PingHandler unit.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?php

declare(strict_types=1);

namespace AppTest\Handler;

use App\Handler\PingHandler;
use Helmich\Psr7Assert\Psr7Assertions;
use Laminas\Diactoros\ServerRequest;
use PHPUnit\Framework\Assert;

class PingHandlerTest extends \AppTest\AbstractTest
{
    use Psr7Assertions;

    public function testPingResponse()
    {
        $pingHandler = new PingHandler();
        $request = new ServerRequest(
            uri: '/api/ping',
            method: 'GET'
        );
        $timestamp = \time();
        $response = $pingHandler->handle($request);

        self::assertThat($response, self::isSuccess());
        self::assertThat($response, self::bodyMatchesJson([
            'ack' => Assert::greaterThanOrEqual($timestamp),
        ]));
    }
}

What’s Next?

The next post in this series introduces static analysis with Psalm.


Series: Mezzio Example

  1. Mezzio Example: Introduction
  2. Mezzio Example: Functional and Unit Testing
  3. Mezzio Example: Psalm Introduction
  4. Mezzio Example: Doctrine Entities and Repositories
comments powered by Disqus