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