Jest is the defacto standard for testing in modern JavaScript but we've traditionally not been able to leverage it for testing in Drupal.

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

by lee.rowlands /

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