Gutenberg contains both PHP and JavaScript code, and encourages testing and code style linting for both.
Why test?
Aside from the joy testing will bring to your life, tests are important not only because they help to ensure that our application behaves as it should, but also because they provide concise examples of how to use a piece of code.
Tests are also part of our code base, which means we apply to them the same standards we apply to all our application code.
As with all code, tests have to be maintained. Writing tests for the sake of having a test isn’t the goal β rather we should try to strike the right balance between covering expected and unexpected behaviours, speedy execution and code maintenance.
When writing tests consider the following:
- What behaviour(s) are we testing?
- What errors are likely to occur when we run this code?
- Does the test test what we think it is testing? Or are we introducing false positives/negatives?
- Is it readable? Will other contributors be able to understand how our code behaves by looking at its corresponding test?
JavaScript testing
Tests for JavaScript use Jest as the test runner and its API for globals (describe, test, beforeEach and so on) assertions, mocks, spies and mock functions. If needed, you can also use React Testing Library for React component testing.
It should be noted that in the past, React components were unit tested with Enzyme. However, React Testing Library (RTL) is now used for all existing and new tests instead.
Assuming you’ve followed the instructions to install Node and project dependencies, tests can be run from the command-line with NPM:
npm test
Linting is static code analysis used to enforce coding standards and to avoid potential errors. This project uses ESLint and TypeScript’s JavaScript type-checking to capture these issues. While the above npm test will execute both unit tests and code linting, code linting can be verified independently by running npm run lint. Some JavaScript issues can be fixed automatically by running npm run lint:js:fix.
To improve your developer workflow, you should setup an editor linting integration. See the getting started documentation for additional information.
To run unit tests only, without the linter, use npm run test:unit instead.
Folder structure
Keep your tests in a test folder in your working directory. The test file should have the same name as the test subject file.
+-- test
| +-- bar.js
+-- bar.js
Only test files (with at least one test case) should live directly under /test. If you need to add external mocks or fixtures, place them in a sub folder, for example:
test/mocks/[file-name].jstest/fixtures/[file-name].js
Importing tests
Given the previous folder structure, try to use relative paths when importing of the code you’re testing, as opposed to using project paths.
Good
import { bar } from '../bar';
Not so good
import { bar } from 'components/foo/bar';
It will make your life easier should you decide to relocate your code to another position in the application directory.
Describing tests
Use a describe block to group test cases. Each test case should ideally describe one behaviour only.
In test cases, try to describe in plain words the expected behaviour. For UI components, this might entail describing expected behaviour from a user perspective rather than explaining code internals.
Good
describe( 'CheckboxWithLabel', () => {
test( 'checking checkbox should disable the form submit button', () => {
...
} );
} );
Not so good
describe( 'CheckboxWithLabel', () => {
test( 'checking checkbox should set this.state.disableButton to `true`', () => {
...
} );
} );
Setup and teardown methods
The Jest API includes some nifty setup and teardown methods that allow you to perform tasks before and after each or all of your tests, or tests within a specific describe block.
These methods can handle asynchronous code to allow setup that you normally cannot do inline. As with individual test cases, you can return a Promise and Jest will wait for it to resolve:
// one-time setup for *all* tests
beforeAll( () =>
someAsyncAction().then( ( resp ) => {
window.someGlobal = resp;
} )
);
// one-time teardown for *all* tests
afterAll( () => {
window.someGlobal = null;
} );
afterEach and afterAll provide a perfect (and preferred) way to ‘clean up’ after our tests, for example, by resetting state data.
Avoid placing clean up code after assertions since, if any of those tests fail, the clean up won’t take place and may cause failures in unrelated tests.
Mocking dependencies
Dependency injection
Passing dependencies to a function as arguments can often make your code simpler to test. Where possible, avoid referencing dependencies in a higher scope.
Not so good
import VALID_VALUES_LIST from './constants';
function isValueValid( value ) {
return VALID_VALUES_LIST.includes( value );
}
Here we’d have to import and use a value from VALID_VALUES_LIST in order to pass:
expect( isValueValid( VALID_VALUES_LIST[ 0 ] ) ).toBe( true );
The above assertion is testing two behaviours: 1) that the function can detect an item in a list, and 2) that it can detect an item in VALID_VALUES_LIST.
But what if we don’t care what’s stored in VALID_VALUES_LIST, or if the list is fetched via an HTTP request, and we only want to test whether isValueValid can detect an item in a list?
Good
function isValueValid( value, validValuesList = [] ) {
return validValuesList.includes( value );
}
Because we’re passing the list as an argument, we can pass mock validValuesList values in our tests and, as a bonus, test a few more scenarios:
expect( isValueValid( 'hulk', [ 'batman', 'superman' ] ) ).toBe( false );
expect( isValueValid( 'hulk', null ) ).toBe( false );
expect( isValueValid( 'hulk', [] ) ).toBe( false );
expect( isValueValid( 'hulk', [ 'iron man', 'hulk' ] ) ).toBe( true );
Imported dependencies
Often our code will use methods and properties from imported external and internal libraries in multiple places, which makes passing around arguments messy and impracticable. For these cases jest.mock offers a neat way to stub these dependencies.
For instance, lets assume we have config module to control a great deal of functionality via feature flags.
// bilbo.js
import config from 'config';
export const isBilboVisible = () =>
config.isEnabled( 'the-ring' ) ? false : true;
To test the behaviour under each condition, we stub the config object and use a jest mocking function to control the return value of isEnabled.
// test/bilbo.js
import { isEnabled } from 'config';
import { isBilboVisible } from '../bilbo';
jest.mock( 'config', () => ( {
// bilbo is visible by default
isEnabled: jest.fn( () => false ),
} ) );
describe( 'The bilbo module', () => {
test( 'bilbo should be visible by default', () => {
expect( isBilboVisible() ).toBe( true );
} );
test( 'bilbo should be invisible when the `the-ring` config feature flag is enabled', () => {
isEnabled.mockImplementationOnce( ( name ) => name === 'the-ring' );
expect( isBilboVisible() ).toBe( false );
} );
} );
Testing globals
We can use Jest spies to test code that calls global methods.
import { myModuleFunctionThatOpensANewWindow } from '../my-module';
describe( 'my module', () => {
beforeAll( () => {
jest.spyOn( global, 'open' ).mockImplementation( () => true );
} );
test( 'something', () => {
myModuleFunctionThatOpensANewWindow();
expect( global.open ).toHaveBeenCalled();
} );
} );
User interactions
Simulating user interactions is a great way to write tests from the user’s perspective, and therefore avoid testing implementation details.
When writing tests with Testing Library, there are two main alternatives for simulating user interactions:
- The
fireEventAPI, a utility for firing DOM events part of the Testing Library core API. - The
user-eventlibrary, a companion library to Testing Library that simulates user interactions by dispatching the events that would happen if the interaction took place in a browser.
The built-in fireEvent is a utility for dispatching DOM events. It dispatches exactly the events that are described in the test spec – even if those exact events never had been dispatched in a real interaction in a browser.
On the other hand, the user-event library exposes higher-level methods (e.g. type, selectOptions, clear, doubleClick…), that dispatch events like they would happen if a user interacted with the document, and take care of any react-specific quirks.
For the above reasons, the user-event library is recommended when writing tests for user interactions.
Not so good: using fireEvent to dispatch DOM events.
import { render, screen } from '@testing-library/react';
test( 'fires onChange when a new value is typed', () => {
const spyOnChange = jest.fn();
// A component with one `input` and one `select`.
render( <MyComponent onChange={ spyOnChange } /> );
const input = screen.getByRole( 'textbox' );
input.focus();
// No clicks, no key events.
fireEvent.change( input, { target: { value: 62 } } );
// The `onChange` callback gets called once with '62' as the argument.
expect( spyOnChange ).toHaveBeenCalledTimes( 1 );
const select = screen.getByRole( 'listbox' );
select.focus();
// No pointer events dispatched.
fireEvent.change( select, { target: { value: 'optionValue' } } );
// ...
Good: using user-event to simulate user events.
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
test( 'fires onChange when a new value is typed', async () => {
const user = userEvent.setup();
const spyOnChange = jest.fn();
// A component with one `input` and one `select`.
render( <MyComponent onChange={ spyOnChange } /> );
const input = screen.getByRole( 'textbox' );
// Focus the element, select and delete all its contents.
await user.clear( input );
// Click the element, type each character separately (generating keydown,
// keypress and keyup events).
await user.type( input, '62' );
// The `onChange` callback gets called 3 times with the following arguments:
// - 1: clear ('')
// - 2: '6'
// - 3: '62'
expect( spyOnChange ).toHaveBeenCalledTimes( 3 );
const select = screen.getByRole( 'listbox' );
// Dispatches events for focus, pointer, mouse, click and change.
await user.selectOptions( select, [ 'optionValue' ] );
// ...
} );
Integration testing for block UI
Integration testing is defined as a type of testing where different parts are tested as a group. In this case, the parts that we want to test are the different components that are required to be rendered for a specific block or editor logic. In the end, they are very similar to unit tests as they are run with the same command using the Jest library. The main difference is that for the integration tests the blocks are run within a special instance of the block editor.
The advantage of this approach is that the bulk of a block editor’s functionality (block toolbar and inspector panel interactions, etc.) can be tested without having to fire up the full e2e test framework. This means the tests can run much faster and more reliably. It is suggested that as much of a block’s UI functionality as possible is covered with integration tests, with e2e tests used for interactions that require a full browser environment, eg. file uploads, drag and drop, etc.
The Cover block is an example of a block that uses this level of testing to provide coverage for a large percentage of the editor interactions.