Automated testing
A few libraries are included with this repo to support some common testing patterns. Additionally, we have adopted the Given-when-then approach for structuring our tests - more info here from Martin Fowler.
In simple terms, you can think of Given-when-then as:
-
Given - Set up the context
-
When - Something happens
-
Then - The change you expect to see
We have found that most test suites actually follow this method, just less defined. E.g. you’ll often find the word “when” in a test name. The given is often just labelled “MyComponent component” and the then has to be inferred by the reader.
We particularly like this way of testing because it standardises a structure which ultimately reduces cognitive load for readers of the tests. A structure also creates efficiency via repetition.
The way we approach writing tests is to first setup all of the Given-when-then statements and then fill in the actual test bodies. You can see this in the two-step example below. This process makes us think of all the parts of the program that we want to test (and how, e.g. some things are better as visual regression or E2E) and means we have little-to-no refactor work to do.
Step 1: Setup the things we want to test
describe('Given the Button component is rendered', () => {describe('when a label is defined', () => {test('then the button should display with that label text', () => {});});describe('when the button is set as disabled', () => {test('then the button should be disabled', () => {});});// ...Continue defining all of the behaviour you wish to test in your suite});
Step 2: Write test bodies
describe('Given the Button component is rendered', () => {describe('when a label is defined', () => {test('then the button should display with that label text', () => {// Fill in the body of the testrender(<Button label="Save" />);expect(screen.getByRole('button', { name: 'Save' })).toBeInTheDocument();});});describe('when the button is set as disabled', () => {test('then the button should be disabled', () => {// Fill in the body of the testrender(<Button label="Save" disabled />);expect(screen.getByRole('button', { name: 'Save' })).toBeDisabled();});});});
- Jest is used for unit testing small pieces of functionality like utility functions
- Test files should sit in the same directory as the file they are testing, and be named with .test before the file extension. E.g. for testing slugify.ts the test file should be named slugify.test.ts
- Tests can be run using the standard Test Commands
- React Testing Library is used to help write integration tests for components
- Like unit tests, test files should sit in the same directory as the component they are testing, and be named with .test before the file extension. E.g. for testing the Field.tsx component the test file should be named Field.test.tsx
- Tests can be run using the standard Test Commands
- Playwright is used for headless end-to-end (e2e) testing and supports all major browsers
- E2E tests should live in a tests folder in the project they are testing
- E2E tests should be named with a .spec.ts extension to separate them from unit and integration tests
- Any package that has e2e tests needs to add its own e2e command to its package.json and to the root folder's package.json. See the docs project's package.json for an example
- Packages with e2e tests will also need a playwright.config.ts config file in their package directory. It can simply import/export the root playwright.config.ts file if no custom configuration is required, but it must have a config file in the package directory to avoid e2e tests being run from other packages.
To run unit and integration tests with Jest use the following commands.
Run all tests:
npm run test
Run tests in watch mode:
npm run test:watch
Run one specific test:
npm run test <path to test>
e.g.
npm run test ./docs/lib/slugify.test.ts