Testing Twig Extensions The Right Way

.com software on 2022-01-05

In the previous article, I wrote how to improve your application’s response time with lazy Twig extensions. Let’s test them properly now.

Before diving into this story make sure you have already read my story on “how to create a Lazy Twig Extension” or that you understand what it is and how it works.

Reduce Symfony response time with lazy Twig extensions Ever wondered why your Symfony app is so slow? One of the factors is going to be the initialization time of the Twig…medium.com

Our app is fast & furious now, using lazy ArticleTagExtension that does its job done. Now is the time to unit test it to ensure our app won’t break.

Installing PHPUnit

Official Twig testing framework expects PHPUnit to be installed. In case we don’t have it installed yet (really?) we’re going to quickly fix the issue php composer.phar req --dev phpunit/phpunit ^8.0 which is going to download a pretty decent version of it.

We’ll add the tests directory for our tests and quickly update the composer.json file to tell PHP where our test classes are:

"autoload-dev": {
  "psr-4": {
    "Test\\App\\": "tests"
  }
}

Creating a test suite

There’s a handy PHPUnit test suite class called IntegrationTestCase that takes the heavy lifting. It’s purpose is to test Twig’s extensions in an isolated environment.

Let’s create the test class in the tests/Twig/Extension directory:

medium-twig-testing-basic-class.php

<?php

declare(strict_types=1);

namespace Test\App\Twig\Extension;

use Twig\Test\IntegrationTestCase;

final class ArticleTagExtensionTest extends IntegrationTestCase
{
    protected function getFixturesDir(): string
    {
        return __DIR__;
    }

    protected function getExtensions(): iterable
    {
        yield new ArticleTagExtension();
    }
}

In its most basic form it requires:

A Twig test is supposed to be in the following format:

--TEST--
Describe here what & why is being tested
--TEMPLATE--
Twig Template of the test
--DATA--
Eval'ed PHP expression to use as the template parameters
--EXPECT--
The expected output of parsed "Template" with the "Data"

Since our extension now uses a runtime, the tests require a runtime loader that will load. Let’s modify the test to enable it. I have written a simple implementation of the ArticleRepository to mock the dependency and rule out database access. You could use mocks, but I prefer solid implementations:

medium-twig-testing-runtime-loader.php

<?php

// ArticleTagExtensionTest.php

protected function getRuntimeLoaders(): iterable
{
      /**
       * The "FactoryRuntimeLoader" class expects an array:
       * array<class-string<T>, closure():T>
       *
       * Our extension declares the Twig function as:
       *     [ArticleTagRuntime::class, 'getTags']
       *
       * This is why the runtime loader will be looking
       * for a runtime named "ArticleTagRuntime::class"
       *
       * Seems complicated? Try first, then play with different values.
       * Or hit me up in the comment. I'll gladly help.
       *
       * This code is > PHP 7.4
       * You can also return an array instead.
       */
      yield new FactoryRuntimeLoader([
          ArticleTagRuntime::class => function (): ArticleTagRuntime {
              return new ArticleTagRuntime(new ImmutableArticleRepository([
                  'dogs', 'cats', 'pigs', 'pigeons', 'monkeys', 'bats',
              ]));
          },
      ]);
}

And the most important part, the test itself! A file named tests/Twig/Extension/function/get_tags.test

--TEST--
get_tags function should return a list of tags

--TEMPLATE--
{{ get_tags(tag_count)|join(', ') }}

--DATA--
return [
    'tag_count' => 5,
]

--EXPECT--
dogs, cats, pigs, pigeons, monkeys

The variable tag_count is unnecessary, acts as a showcase of how to use test parameters. If you don’t wish to use any, you can simply leave return []

As soon as we run the tests (the --exclude-group of legacy is necessary to avoid an unused test case which would otherwise be reported as “skipped”):

vendor/bin/phpunit --exclude-group=legacy --bootstrap vendor/autoload.php tests

We end up green!

OK (1 test, 2 assertions)

Have any questions? Feel free to hit me up in the comments. The code is available publicly under an MIT license on GitHub. Happy testing!

Psst… want to know how to make use of the “yield” keyword in a real-life scenario? See my other publication.

Iterating over billions of objects in Doctrine Having millions or billions of objects to iterate over in PHP using Doctrine? How to do this with a limited amount of…medium.com