What is your testing approach when working on fast-pace projects?
Tags: react, discuss, javascript, webdev
Published: 2021-12-30 14:49:58

Hey guys, I'm wondering how you tackle testing React-based apps. Particularly, I'd like to hear your thoughts about testing rapidly changing products like MVPs. 

For a long time I was a big fan of e2e tests. However, many of my past teams struggled setting them up or/and were underestimating their value. Instead, the most common way of testing I observed is unit (I suppose) testing with jest + testing library + axios-mock-adapter (or some other request mocking libs). And here is my inner struggle: in my opinion, very granular unit testing on a MVP isn't the most efficient as often its implementation radically changes. I believe the main purpose of tests on MVP is to lock the current state of UI so the future implementation changes don't break what's been already working. Of course, one will argue that the more tests the better, but the reality is that we need to choose what will work best in a given time-frame (often very limited). Therefore, I worked out my own pattern which is a sort of hybrid:

- I test entire pages (mocking routing)
- I mock auth-related action(s)
- I mock actions which manipulate URL
- I even mock Web Workers if necessary
- I mock all AJAX requests with axios-mock-adapter in a way which lets me wait for those calls (a combination of spies and waitFor)
- My tests are driven by AJAX calls i.e. it's AJAX calls which indicate when certain interaction has been completed
- I often use snapshots and treat them carefully when they fail

See this stripped-out real world example:

```javascript
import React from 'react';
import { ExamplePage } from '../pages';
import { screen, waitFor, fireEvent } from '@testing-library/react';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import mocks from '../mocks/someCollectionEdit.json';
import renderPage from './helpers/renderPage';

const API_BASE_URL = '/api';

jest.mock('../actions/getters/user.ts', () => {
  const actions = jest.requireActual('../actions/getters/user.ts');

  actions.authenticateUser = jest.fn();

  return actions;
});

jest.mock('../workers/someWorker/someWorker.client.ts');
jest.mock('../actions/setters/url.ts');

describe('render example page', () => {
  let mock;

  const mockRequests = () => {
    // used by waitFor() in tests
    const spies = {
      [`${API_BASE_URL}/user`]: jest.fn(),
      [`${API_BASE_URL}/organizations`]: jest.fn(),
      [`${API_BASE_URL}/some-collection/example-id?someFilter=filter1&organizationId=2`]: jest.fn(),
      [`${API_BASE_URL}/some-filters/example-id`]: jest.fn(),
      [`${API_BASE_URL}/some-collection/details/example-id`]: jest.fn(),
      // ...
    };

    // mocking calls which may include query strings
    ((url) =>
      mock.onGet(url).reply((config) => {
        process.nextTick(() => spies[config.url]());
        return [200, mocks[config.url]];
      }))(new RegExp(`${API_BASE_URL}/user$`));
    ((url) =>
      mock.onGet(url).reply((config) => {
        process.nextTick(() => spies[config.url]());
        return [200, mocks[config.url]];
      }))(new RegExp(`${API_BASE_URL}/organizations$`));
    ((url) =>
      mock.onGet(url).reply((config) => {
        process.nextTick(() => spies[config.url]());
        return [200, mocks[config.url]];
      }))(
      new RegExp(
        `${API_BASE_URL}/some-collection/example-id\\?.*`,
      ),
    );
    ((url) =>
      mock.onGet(url).reply((config) => {
        process.nextTick(() => spies[config.url]());
        return [200, mocks[config.url]];
      }))(
      new RegExp(
        `${API_BASE_URL}/some-filters/example-id$`,
      ),
    );
    ((url) =>
      mock.onPost(url).reply((config) => {
        process.nextTick(() => spies[config.url]());
        return [200, mocks[config.url]];
      }))(
      new RegExp(
        `${API_BASE_URL}/some-collection/example-id/data-draft$`,
      ),
    );
    ((url) =>
      mock.onPut(url).reply((config) => {
        process.nextTick(() => spies[config.url](), 0);
        return [200, mocks[config.url]];
      }))(
      new RegExp(
        `${API_BASE_URL}/some-collection/example-id/data$`,
      ),
    );
    // ...

    return spies;
  };

  beforeAll(() => {
    mock = new MockAdapter(axios);
  });

  afterEach(() => {
    mock.reset();
  });

  it('should edit some form with a confirmation modal', async () => {
    const spies = mockRequests();

    renderPage(ExamplePage, {
      route: '/organizations/:organizationId/some-collection/:collectionId/record/edit',
      url: '/organizations/2/some-collection/example-id/record/edit',
      search: '?someFilter=filter1',
    });

    await waitFor(() => // page has been rendered with all the necessary data
      expect(
        spies[
          `${API_BASE_URL}/some-collection/example-id?someFilter=filter1&organizationId=2`
        ],
      ).toHaveBeenCalledTimes(1),
    );

    const inputField = screen.getByDisplayValue(/example value/i);
    const saveChangesButton = screen.getByText(/Save changes/i);

    fireEvent.change(inputField, { target: { value: 'updated value' } }); // user action
    fireEvent.click(saveChangesButton); // user action

    await waitFor(() => // data draft has been sent
      expect(
        spies[
          `${API_BASE_URL}/some-collection/example-id/data-draft`
        ],
      ).toHaveBeenCalledTimes(1),
    );

    expect(screen.getByText(/Save some collection changes changes\?/i)).toBeInTheDocument();
    expect(screen.getByText(/updated value/i)).toBeInTheDocument();

    fireEvent.click(screen.getByText(/Confirm/i)); // user action

    await waitFor(() => // data has been submitted
      expect(
        spies[
          `${API_BASE_URL}/some-collection/example-id/data`
        ],
      ).toHaveBeenCalledTimes(1),
    );

    expect(
      screen.getByText(
        /Some collection records has been successfully changed./i,
      ),
    ).toBeInTheDocument();
  });

  // ...
});
```

Please share your thoughts about this matter and feel free to criticise my approach and suggest what would be better based on your commercial experience. Also, Happy New Year!