Lee RowlandsSenior Developer
Testing Twig templates and custom JavaScript with Jest
Jest is the defacto standard for testing in modern JavaScript, but until now, we haven't been able to leverage it for testing in Drupal. Thanks to twig-testing-library, that's changed.
With twig-testing-library, we can now test our twig templates and any dynamic behaviours added by Javascript using Jest.
In this article we will go through the process of adding Jest based testing to an existing accordion component.
Installation
Firstly we need to install twig-testing-library and jest
npm i --save-dev twig-testing-library jest
And we're also going to add additional dom-based Jest asserts using jest-dom
npm i --save-dev @testing-library/jest-dom
Now we need to configure Jest by telling it how to find our tests as well as configuring transpiling.
In this project, we've got all of our components in folders in a /packages sub directory.
So we create a jest.config.js file in the root with the following contents:
// For a detailed explanation regarding each configuration property, visit:
// https://jestjs.io/docs/en/configuration.html
module.exports = {
clearMocks: true, // Clear mocks on each test.
testMatch: ['/packages/**/src/__tests__/**.test.js'], // How to find our tests.
transform: {
'^.+\\.js?$': `/jest-preprocess.js`, // Babel transforms.
},
setupFilesAfterEnv: [`/setup-test-env.js`], // Additional setup.
};
For transpiling we're just using babel-jest and then chaining with our projects presets. The contents of jest-preprocess.js is as follows:
const babelOptions = {
presets: ['@babel/preset-env'],
};
module.exports = require('babel-jest').createTransformer(babelOptions);
As we're going to also use the Jest dom extension for additional Dom based assertions, our setup-test-environment takes care of that as well as some globals that Drupal JS expects to exist. The contents of our setup-test-env.js file is as follows:
import '@testing-library/jest-dom/extend-expect';
global.Drupal = {
behaviors: {},
};
Writing our first test
Now we have the plumbing done, let's create our first test
As per the pattern above, these need to live in a __tests__ folder inside the src folder of our components
So let's create a test for the accordion component, by creating packages/accordion/src/__tests__/accordion.test.js
Let's start with a basic test that the accordion should render and match a snapshot. This will pickup when there are changes in the markup and also verify that the template is valid.
Here's the markup in the twig template
<div class="accordion js-accordion">{% block button %}<button class="button button--primary accordion__toggle">{{ title | default('Open Me') }}</button>{% endblock %}<div class="accordion__content">{% block content %}<h1>Accordion Content</h1><p>This content is hidden inside the accordion body until it is disclosed by clicking the accordion toggle.</p>{% endblock %}</div></div>
So let's render that with twig-testing-library and assert some things in packages/accordion/src/__tests__/accordion.test.js
import { render } from 'twig-testing-library';
describe('Accordion functionality', () => {
it('Should render', async () => {
expect.assertions(2);
const { container } = await render(
'./packages/accordion/src/accordion.twig',
{
title: 'Accordion',
open: false,
},
);
expect(container).toMatchSnapshot();
expect(container.querySelectorAll('.accordion__toggle')).toHaveLength(1);
});
});
Running the tests
So let's run our first test by adding a jest command to our package.json under "scripts"
"jest": "jest --runInBand"
Now we run with
npm run jest
> jest --runInBand
PASS packages/accordion/src/__tests__/accordion.test.js
Accordion functionality
✓ Should render (43 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 1 passed, 1 total
Time: 4.62 s, estimated 6 s
Ran all test suites.
Testing dynamic behaviour
Now we know our template renders, and we're seeing some expected output, let's test that we can expand and collapse our accordion.
Our accordion JS does the following:
- On click of the accordion title, expands the element by adding accordion--open class and sets the aria-expanded attribute
- On click again, closes the accordion by removing the class and attribute
So let's write a test for that - by adding this to our existing test:
it('Should expand and collapse', async () => {
expect.assertions(4);
const { container, getByText } = await render(
'./packages/accordion/src/accordion.twig',
{
title: 'Open accordion',
},
);
const accordionElement = container.querySelector(
'.accordion:not(.processed)',
);
const accordion = new Accordion(accordionElement);
accordion.init();
const accordionToggle = getByText('Open accordion');
fireEvent.click(accordionToggle);
expect(accordionElement).toHaveClass('accordion--open');
expect(accordionToggle).toHaveAttribute('aria-expanded', 'true');
fireEvent.click(accordionToggle);
expect(accordionElement).not.toHaveClass('accordion--open');
expect(accordionToggle).toHaveAttribute('aria-expanded', 'false');
});
Now let's run that
npm run jest
packages/accordion/src/__tests__/accordion.test.es6.js
Accordion functionality
✓ Should render (29 ms)
✓ Should expand and collapse (20 ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 1 passed, 1 total
Time: 5.031 s, estimated 6 s
Ran all test suites.
Neat! We now have some test coverage for our accordion component
Next steps
So the neat thing about Jest is, it can collect code-coverage, let's run that
npm run jest -- --coverage
packages/accordion/src/__tests__/accordion.test.es6.js
Accordion functionality
✓ Should render (28 ms)
✓ Should expand and collapse (13 ms)
-------------------|---------|----------|---------|---------|--------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-------------------|---------|----------|---------|---------|--------------------
All files | 29.55 | 11.27 | 24.14 | 30 |
accordion/src | 100 | 85.71 | 100 | 100 |
accordion.es6.js | 100 | 85.71 | 100 | 100 | 53
base/src | 11.43 | 3.13 | 4.35 | 11.65 |
utils.es6.js | 11.43 | 3.13 | 4.35 | 11.65 | 14,28,41-48,58-357
-------------------|---------|----------|---------|---------|--------------------
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 1 passed, 1 total
Time: 2.813 s, estimated 5 s
Ran all test suites.
Pretty nice hey?
What's happening behind the scenes
If you've worked on a React project before, you've probably encountered Dom testing library and React testing library. Twig testing library aims to provide the same developer ergonomics as both of these libraries. If you've familiar with either of those, you should find Twig testing library's API's comparable.
Under the hood it's using Twig.js for Twig based rendering in JavaScript and Jest uses jsdom for browser JavaScript APIs.
A longer introduction
I did a session on using this for a Drupal South virtual meetup, here's the recording of it.
Get involved
If you'd like to get involved, come say hi on github.