UI Unit testing with Jest/Bigtest

UI Unit testing with Jest/Bigtest

Jest (UI Testing Tool Evaluation Criteria)

  1. Speed - Fast

  2. Reliability - High

  3. Relevance - High

  4. Mocking facility - library can be mocked or functions that make HTTP requests. or via libs like `nock` etc

  5. 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).

  6. no

  7. yes (3 files in parallel, but not test cases)

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