Skip to end of banner
Go to start of banner

UI Unit testing with Jest/Bigtest

Skip to end of metadata
Go to start of metadata

You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 9 Current »

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

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.


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