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-testidas 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();
});
});
});