Testing front-end code

We use Jest to test front end code. See the Jest documentation for how to use Jest and write assertions.

You can run the front-end tests with npm run tui-test.

You can run a single test in watch mode with npm run tui-test -- --watch MyTestName.

Testing JS

Aim for high test coverage of .js files, especially in tui core, as code there is depended on by many other parts of the codebase. You can generate a coverage report with npx jest --coverage.

Write primarily unit tests rather than integration tests – mock/stub dependencies.

Structuring tests

If you're testing code in a file named foo.js, the tests should be located in __tests__/foo.spec.js under the same folder. Keeping the test and the code under test co-located makes it easier to understand where tests are located.

For each export you're testing, wrap with a describe('name', () => {})  block. If it's the default export, you can use the file name or class name as the name.

Inside the describe(), use it() to define individual tests – it should read as a natural English sentence with the "it" prepended to it, e.g. it('returns the sum of both numbers') .

Testing Vue components

We write component tests using Testing Library. This is the current industry standard for testing UI components. Testing Library has good documentation, so it’s worth having a read through.

Testing Library has been wrapped with our own extra functionality, so should be imported as tui_test_utils/vtl.

What to test

For Vue components, the approach we want to take is black box testing components as a user would use them. Test the component by interacting with it, rather than calling internal methods.

This means a big part of our component tests is finding elements in the DOM and interacting with them, or asserting that they are as we expect. Testing Library has an excellent suite of tools for this, the preferred method being to query elements by their role + accessible name, for example: screen.getByRole('button', { name: /delete_user/ }). This solves a major shortcoming with vue-test-utils, and is also better than what Behat provides (see the section on Behat below).

Available queries: About Queries | Testing Library

Available Jest matchers: testing-library/jest-dom: Custom jest matchers

In our component tests, we can mock JS services but do not generally mock other UI components unless they are complex dependencies like CourseAdder.

The above guidelines aren’t going to make sense for every single component, for example, things that are technically Vue components but are almost entirely pure JS logic such as Reform are likely not going to be tested with Testing Library, or in this way. Use your judgement, but default to using Testing Library.

If there is complex logic that needs to be directly tested, consider extracting it to a JS file or exporting it as additional exports from the component, and testing it separately from the UI.

Workflow

First, create a test file in a directory named __tests__ next to the component. E.g. if you are testing FooBar.vue, create __tests__/FooBar.spec.js.

You can use the following skeleton:

import FooBar from '../FooBar'; import { fireEvent, render, screen, waitFor } from 'tui_test_utils/vtl'; describe('FooBar', () => { it('does something', async () => { const view = render(FooBar, { props: {} }); screen.logTestingPlaygroundURL(); }); });

If you run this test, you will get a link to the Testing Library playground with the HTML of your rendered component preloaded. You can then use the picker to click elements, and it will suggest a selector for you to use, e.g. getByRole('button', { name: /\[\[cancel, core\]\]/i }). This has some unnecessary bits in, so we could reduce that down to getByRole('button', { name: /cancel/ }). If we call this on screen, it will return a DOM element.

We can then take that and pass it as an argument to one of the methods on fireEvent to interact with the page, or pass it to expect() to assert something about the element.

For example:

it('allows removing elements', async () => { const view = render(FooBar); await fireEvent.click(screen.getByRole('button', { name: /remove_x, core, "Potato"/ })); expect(screen.queryByText(/Potato/)).not.toBeInTheDocument(); });

Querying DOM elements

It’s best to target elements semantically using the queries provided by Testing Library. This both helps make the test resistant to breakage if the underlying implementation details change, and helps ensure components are accessible. Try to avoid using querySelector and friends unless absolutely necessary.

If you have an element that you need to query and there is not an obvious way to query for it using Testing Library selectors, for example <div class="tui-myEl__count">3</div>, instead of finding it by the CSS class (which is fragile), add a data-testid attribute and use getByTestId.

Comparison to vue-test-utils

The Vue integration for Testing Library uses vue-test-utils under the hood, but using vue-test-utils directly is not usually convenient. The only built in way it has to find elements is with a CSS selector, which is fairly limited by itself (e.g. you can’t find a button with the text “Submit”), and encourages searching by CSS class, which is more fragile.

A note on Behat

Behat is a way to interact with the distribution as a whole and write end-to-end tests, however it has some significant limitations.

Querying elements in Behat is a poor experience, as it generally cannot find elements by their accessible label, often requiring custom steps to be written or CSS selectors to be used. Behat is often fragile, slow, hard to debug, and overly coupled to a specific implementation of the code under test.

Try and write most of your UI tests using Jest and Testing Library, and only minimal tests using Behat to verify that the system as a whole works. Follow the Testing Pyramid.

Dos and Don’ts

Do:

  • Prefer Testing Library over using vue-test-utils directly

  • Find elements by role + text/label or data-testid