UI Unit testing with Jest/Bigtest
Jest (UI Testing Tool Evaluation Criteria)
- Speed - Fast
- Reliability - High
- Relevance - High
- Mocking facility - library can be mocked or functions that make HTTP requests. or via libs like `nock` etc
- Cost to migrate/rebuild existing tests. based on module: it can be few weeks and months (e.g ui-users) for modules that follow Page patterns (huge components).
- no
- yes (3 files in parallel, but not test cases)
- yes
React Testing Library with jest
its props/cons is a reflection of jest pros/cons.
On top of that:
- is a light-weight solution for testing React components
- provides light utility functions in a way that encourages devs to write tests that closely resemble how web pages are used
- rather than dealing with instances of rendered React components, tests will work with actual DOM nodes.
- finding form elements by their label text (just like a user would), finding links, and buttons from their text (like a user would). It also exposes a recommended way to find elements by a
data-testid
as an "escape hatch" for elements where the text content and label do not make sense or is not practical.
Additional resources:
https://github.com/testing-library/user-event
https://testing-library.com/docs/react-testing-library/intro
https://blog.sapegin.me/all/react-testing-3-jest-and-react-testing-library/
Bigtest
Pros
- Used by FOLIO community
Cons
Look at Jest Pros
Example
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { describe, beforeEach, it } from '@bigtest/mocha';
import {
interactor,
Interactor,
} from '@bigtest/interactor';
import { expect } from 'chai';
import faker from 'faker';
import sinon from 'sinon';
// eslint-disable-next-line
import { mountWithContext } from '@folio/stripes-components/tests/helpers';
import QuickMarcEditor from './QuickMarcEditor';
const QuickMarcEditorInteractor = interactor(class {
static defaultScope = '#quick-marc-editor-pane';
closeButton = new Interactor('[data-test-cancel-button]')
});
const getInstance = () => ({
id: faker.random.uuid(),
title: faker.lorem.sentence(),
});
const marcRecord = {
records: [{
tag: '001',
content: '$a as',
}, {
tag: '245',
content: '$b a',
}],
};
// mock components? seems no, babel-plugin-rewire should be added
describe('QuickMarcEditor', () => {
const instance = getInstance();
let onClose;
const quickMarcEditor = new QuickMarcEditorInteractor();
beforeEach(async () => {
onClose = sinon.fake();
await mountWithContext(
<MemoryRouter>
<QuickMarcEditor
instance={instance}
onClose={onClose}
onSubmit={sinon.fake()}
initialValues={marcRecord}
/>
</MemoryRouter>,
);
});
it('should be rendered', () => {
// eslint-disable-next-line
expect(quickMarcEditor.isPresent).to.be.true;
});
// new async action? new branch
describe('clsoe action', () => {
beforeEach(async () => {
await quickMarcEditor.closeButton.click();
});
it('onClose prop should be invoked', () => {
// eslint-disable-next-line
expect(onClose.callCount !== 0).to.be.true;
});
});
// another prop is requried? the second render
describe('disable', () => {
beforeEach(async () => {
await mountWithContext(
<MemoryRouter>
<QuickMarcEditor
instance={instance}
onClose={onClose}
onSubmit={sinon.fake()}
initialValues={{}}
/>
</MemoryRouter>,
);
});
// no async in cases
it('onClose prop should be invoked', () => {
// eslint-disable-next-line
expect(true).to.be.true;
});
});
});
Jest
Pros
- Recommended by React team https://reactjs.org/docs/testing.html#tools
- Good documentation with examples - easy to learn
- Big community
- Active development
- Doesn't require additional dependencies like chai and sinon for mocha, everything is already included
- Good API
- Mocking is easy
- Async test cases (in case of BigTest all async operations like render and actions should be done in before* blocks, with it we have many inner describe blocks and not required rerenders)
- It's fast
Cons
- common mocks are required as tests don't render in real browser
Example
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { render, cleanup, act, fireEvent } from '@testing-library/react';
import { IntlProvider } from 'react-intl';
import faker from 'faker';
import '@folio/stripes-acq-components/test/jest/__mock__';
import QuickMarcEditorContainer from './QuickMarcEditorContainer';
const getInstance = () => ({
id: faker.random.uuid(),
title: faker.lorem.sentence(),
});
const match = {
params: {
instanceId: faker.random.uuid(),
},
};
const record = {
id: faker.random.uuid(),
leader: faker.random.uuid(),
fields: [],
};
const messages = {
'ui-quick-marc.record.edit.title': '{title}',
};
const renderQuickMarcEditorContainer = ({ onClose, mutator }) => (render(
<IntlProvider locale="en" messages={messages}>
<MemoryRouter>
<QuickMarcEditorContainer
onClose={onClose}
match={match}
mutator={mutator}
/>
</MemoryRouter>
</IntlProvider>,
));
describe('Given Quick Marc Editor Container', () => {
let mutator;
let instance;
beforeEach(() => {
instance = getInstance();
mutator = {
quickMarcEditInstance: {
GET: () => Promise.resolve(instance),
},
quickMarcEditMarcRecord: {
GET: jest.fn(() => Promise.resolve(record)),
PUT: jest.fn(() => Promise.resolve()),
},
};
});
afterEach(cleanup);
it('Than it should fetch MARC record', async () => {
await act(async () => {
await renderQuickMarcEditorContainer({ mutator, onClose: jest.fn() });
});
expect(mutator.quickMarcEditMarcRecord.GET).toHaveBeenCalled();
});
it('Than it should display Quick Marc Editor with fetched instance', async () => {
let getByText;
await act(async () => {
const renderer = await renderQuickMarcEditorContainer({ mutator, onClose: jest.fn() });
getByText = renderer.getByText;
});
expect(getByText(instance.title)).toBeDefined();
});
describe('When close button is pressed', () => {
it('Than it should invoke onCancel', async () => {
let getByText;
const onClose = jest.fn();
await act(async () => {
const renderer = await renderQuickMarcEditorContainer({ mutator, onClose });
getByText = renderer.getByText;
});
const closeButton = getByText('stripes-acq-components.FormFooter.cancel');
expect(onClose).not.toHaveBeenCalled();
fireEvent(closeButton, new MouseEvent('click', {
bubbles: true,
cancelable: true,
}));
expect(onClose).toHaveBeenCalled();
});
});
});