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
is a light-weight solution for testing React components. It 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.
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();
});
});
});