import { Meta } from '@storybook/addon-docs';
Tests can fortify new features from breaking changes, help prevent bugs from being reintroduced, and overall future-proof against an ever-changing ecosystem. As more people get involved and contribute, they can be confident in fixing or introducing changes without breaking existing functionality.
When we write tests, we need to make sure that they are resilient to change as well as being easily understandable should somebody need to add or adjust tests as other features are introduced and bugs are fixed.
Each component's directory should contain a tests
directory that
contains test files named with the component name, and sometimes a
specific test suite name. The tests
directory should also contain an
often quite useful interactor.js
file. For example, the directory
structure for <Button>
looks like this:
Button
|-Button.js
|-Button.css
|-index.js
|-readme
|-tests
| |-interactor.js
| |-Button-test.js
Anytime you change or add features, you should be changing or adding tests.
If you are making a change to existing functionality, existing tests should be updated. If there were no existing test to update, tests should be added. If you are adding new features, you should write plenty of tests for those new features so that you can rest assured nobody else (or even yourself) accidentally breaks it in the future.
Anytime you fix a bug, you should be adding new tests or adding to existing tests.
Bugs can obviously be a real pain. Even "simple" bugs can keep popping up repeatedly as seemingly unrelated things change in the code. When you fix a bug, you most likely want it fixed for good. Writing tests or fortifying existing tests makes it easily known if the bug is accidentally introduced again when said tests fail.
Write tests first!!!
If you're writing a feature or fixing a bug, write or change some tests first so that they fail expectedly, or the bug is successfully reproduced. Then as you start adding or making changes, and your tests start passing one-by-one, you know you're headed in the right direction until they all pass when the feature is complete or the bug is fixed.
Tests in stripes components are written using Mochaand interactions and assertions are performed via Bigtest interactors
A good place to start is by looking at existing tests. If the
component you're working on already has tests, you can add new tests
to existing setup hooks, or copy similar sections and change relevant
pieces. If the component does not have existing tests, check out
another component's tests, like the Button
component,
and follow the same patterns but specific to the component you're
working on.
The patterns themselves can be outlined in a few simple steps:
- Do any setup work necessary
- Perform any actions on the component that we want to verify
- Make assertions against the desired state
Here's a sample of tests from the Button
component:
import React from 'react';
// testing tools
import { describe, beforeEach, it } from 'mocha';
import { expect } from 'chai';
// test helpers
import { mount } from '../../../tests/helpers';
// the component to test
import Button from '../Button';
// interactor used to perform actions (more on this later)
import { Button as ButtonInteractor } from '@folio/stripes-testing';
// the actual tests
describe('Button', () => {
const button = ButtonInteractor();
let clicked;
// 1. Do any setup work necessary
beforeEach(async () => {
// set to false before each test
clicked = false;
// mount our component with props
await mount(
<Button onClick={() => { clicked = true; }} id="button-test">
test
</Button>
);
});
// make assertions against the initial rendered state
// interactors allow this through their `is\has` API.
it('renders a <button> tag', async () => {
await button.is({ button: true });
});
it('renders with default class', async () => {
await button.perform((e) => expect(e.classList.contains(css.default)).to.be.true);
});
it('renders child text as its label', async () => {
await button.has({ text: 'test' });
});
it('has an id on the root element', async () => {
await button.has({ id: 'button-test' });
});
// testing a specific action
describe('clicking the button', () => {
// perform the action...
beforeEach(async () => {
await button.click();
});
it('calls the onClick handler', () => {
expect(clicked).to.be.true;
});
});
});
We use a helper called mount
in our first beforeEach
hook. This
helper will clean up and teardown any previously mounted component,
then mount our new component into the DOM for us. We use the
async
/await
syntax to wait for React to mount our component so the
tests don't start running too early. Since this helper cleans up
previous components on the next call, this allows us to investigate
and debug our tests after they run. You can then use Mocha's it.only
to isolate a specific test to debug.
In the previous example, all of our tests reference the button = ButtonInteractor()
at the top of the test suite which is imported
from [the button's interactor in @folio/stripes-testing]
file](https://github.com/folio-org/stripes-testing/blob/master/interactors/button.js).
import HTML from './baseHTML';
export default HTML.extend('button')
.selector('a[href],button,input[type=button],input[type=submit],input[type=reset],input[type=image],a[role=button],div[role=button]')
.filters({
// some buttons don't have attribute href
href: (el) => el.getAttribute('href') ?? '',
type: (el) => el.getAttribute('type'),
icon: (el) => el.getAttribute('icon'),
button: (el) => el.tagName === 'BUTTON',
anchor: (el) => el.tagName === 'A',
default: (el) => el.classList.contains('default'),
ariaLabel: (el) => el.ariaLabel,
ariaExpanded: (el) => el.getAttribute('aria-expanded'),
disabled: {
apply: (el) => {
if (el.disabled !== undefined) return el.disabled;
return el.getAttribute('aria-disabled') === 'true';
},
default: false
}
});
You should always use an interactor if your tests need to interact with the DOM either by reading attributes, properties, text, classnames, or by sending actions such as click, focus, blur, or change events.
An Interactor
is a powerful, customizable, composable, page
object. This adds a
layer of durability to our suite because when things like classnames
or the markup inevitably change, we should only need to update our
interactor as opposed to updating each of our tests. Interactors can
also be composed by each other, so if another component uses a button,
that component's interactor can use the ButtonInteractor
too.
Interactors are also convergent and will wait for elements to exist in the DOM before interacting with them. Interactor properties are lazy and do not query for the element until they are accessed. To learn more about what they do, how to create your own interactors, and how to then compose interactors, check out the BigTest Interactor Guides.
Components that render translated strings will require intl context. This
is conveniently provided in the mountWithContext
helper.
// instead of importing `mount`, import `mountWithContext`
import { mountWithContext } from '../../../tests/helpers';
// ...
beforeEach(async () => {
await mountWithContext(
<Datepicker />
);
});