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: https://testing-library.com/docs/queries/about
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
Prefer writing Jest + Testing Library tests over Behat
Write mainly black-box tests -- do not reach in to the internals of the component and inspect computed properties and call methods, unless they are part of the public API of the component (such as .update() on Uniform). Instead, verify what the component renders and what happens when interacting with it.
Try to cover component logic rather than just what the template renders
Don’t:
Use snapshot tests
Reference CSS classes in tests
Test features of the Vue framework itself. You can trust that (for example) text you hard code in the template, will be rendered. Only test things like that if they are conditional — and what you’re really testing there is the condition.
Render the component in a beforeEach(), as this limits what you can test in each test case. If you find yourself with shared code between tests, extract it to a local function instead.
Write tests for components with no logic, such as Separator. There's nothing to test, so they add no value.
Example tests to reference
Basic component test:
client/component/tui/src/components/basket/__tests__/Basket.spec.js
GraphQL mocking:
client/component/mfa_totp/src/components/__tests__/Register.spec.js
Find more by searching for
tui_test_utils/vtl
Gotchas
Jest runs tests using jsdom instead of a real browser. This makes tests much faster, but does come with some small oddities such as HTML elements not having layout, no scrolling support, and window.location not doing anything. Generally these don’t pose much of a problem.
Recipe book
Locating an input by its label and changing its value
await fireEvent.update( screen.getByRole('textbox', { name: /course_name/ }), 'Architecture of Electrical Transmission Towers in Kralovec' );
Verifying the DOM is as expected
expect(screen.getByText(/My hovercraft is full of eels/i)).toBeInTheDocument(); expect(screen.getByRole('button', { name: /add_potato/ })).toBeDisabled();
Verifying that an event is emitted
const view = render(MyComponent); await fireEvent.click(view.getByRole('button', { name: /remove_potato/ })); expect(view.emitted()).toEqual({ 'do_thing': [[123]] });
Mocking GraphQL queries
const view = render(Register, { mockQueries: [ { request: { query: createInstanceMutation, variables: { input: { secret, token: '123456' } }, }, result: { data: { instance: { id: 123 } } }, }, ], });
You can also pass a function as result
.
Checking a redirect
const setLocation = jest.fn(); Object.defineProperty(window, 'location', { set: setLocation, configurable: true, }); await fireEvent.click(screen.getByRole('button', { name: /save/i })); expect(setLocation).toHaveBeenCalledWith('http://localhost/foo/bar.php');
Waiting for an async change to happen
await fireEvent.click(view.getByRole('button', { name: /submit/ })); await waitFor(() => { expect(screen.getByText(/invalid_potato/)).toBeInTheDocument(); });
Waiting until all microtasks have been processed
Microtasks are created when you execute async functions or by calling .then on a promise. You can use flushMicrotasks() to wait for the queue of microtasks to all be executed before continuing.
Note that this will not wait for timeouts (created with setTimeout) etc to run.
If testing a Vue component, consider using waitFor()
instead.
import { flushMicrotasks } from 'tui_test_utils'; /* ... */ foo.doThing(); // does thing in an async function await flushMicrotasks(); expect(foo.thingDone).toBeTrue();
Extracting common code from tests to avoid repetition
function renderThingymabob(options = {}) { return render(Thingymabob, { ...options, props: { title: 'Thingymabob', ...options.props, }, }); } async function submitForm(token) { await fireEvent.update(screen.getByRole('textbox', { name: /verify/ }), token); await fireEvent.click(screen.getByRole('button', { name: /save/ })); } describe('Thingymabob', () => { it('does the thing', async () => { const view = renderThingymabob(); await submitForm('hello'); }); });