DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

chris-czopp
chris-czopp

Posted on

What is your testing approach when working on fast-pace projects?

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:

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

  // ...
});
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (2)

Collapse
 
mrdulin profile image
official_dulin • Edited on

Testing rapidly changing products like MVPs:

  1. Only unit testings - The lowest cost and simplest, mock every side effect code such as network I/O, promise, timer etc..
  2. Core business logic - The most important, data structures and algorithms, business processes.
  3. No UI testing for component - Unless your application is UI heavy, third party, fully tested component libraries are generally used. Or, just make some snapshot testing to test the component tree(DOM structure) when the states(data) are changed.
Collapse
 
jakecarpenter profile image
Jake Carpenter

I very much agree. Tests that focus on a spy of some prop function being called aren’t going to do you any good when a new requirement means you need to hoist that function into a context or hook. Those are implementation details and I don’t care if the function is called or even exists.

I focus on screens as well and ensure the screen can go through loading states as data comes in. From there, you can find ways to abstract your code that lets you test UI elements without the data loading, but interactions are better tested further up where they can provide protection and still allow refactoring.

Join us at DEV
Yes, this is technically an β€œad”, but really we just want to ask if you want to join DEV. We have 900k+ developers reading, posting, and enjoying community, and would love to have you. Β  Create an account and continue your coding journey.