Skip to content

Unit Tests

Ievgen Lebediev edited this page Apr 14, 2021 · 9 revisions

Info

Unit tests - are type of test for individual units or components of the app. A unit might be an individual function, method, procedure, module or object.

Why write tests?

  • Documentation: tests are a specifications for how a code should work.
  • Consistency: verify that engineers follows best practices and conventions of a team.
  • Productivity: test allow programmers to ship quality code faster and in more reliable way; to get an error if you break code.

Testing environment

To start testing we need to choose a testing structure that suits us, namely to choose test runner, assertion library and some test util if any. Some frameworks like Jest, Jasmine provide most of these out of the box. Some of them provide only some of the functionality and a combination of libraries can be used. We need to concentrate in the following project tech stack, in order to choose right framework/library/util.

Project basic technology stack (frontend part of SPA):

  • React(using functional components)/Typescript;
  • Redux/Redux Toolkit (state management);
  • React Router (navigation);
  • Material UI (WEB UI components).

Let's review most popular frameworks to compare their advantages and make a choice:

Jest

  • Performance - first of all Jest is considered to be faster for big projects with many test files by implementing a clever parallel testing mechanism.
  • UI - clear and convenient.
  • Ready-To-Go - comes with assertions, spies, and mocks. Libraries still can easily be used in case you need some unique features.
  • Globals - like in Jasmine, it creates test globals by default so there is no need to require them. This can be considered bad since it makes your tests less flexible and less controllable, but in most cases it just makes your life easier.
  • Great modules mocking - jest provides you with an easy way to mock heavy modules to improve testing speed. For example, a service can be mocked to resolve a promise instead of making a network request.
  • Code coverage - includes a powerful and fast built-in code coverage tool that is based on Istanbul.
  • Reliability - since it has a huge community, and used in many very complex projects, it is considered very reliable. It is currently supported by all the major IDEs and tools.
  • Support - has widespread Facebook support for all its versions and it is recommended in the official React documentation.
  • Development - Jest only updates the files updated, so tests are running very fast in watch mode.

Jasmine

  • Ready-To-Go - comes with everything you need to start testing.
  • Globals - comes with all the important testing features in the global scope well.
  • Community - it has been on the market since 2009 and gathered a vast amount of articles, suggestions and tools that are based on it.
  • Support - has widespread Angular support for all its versions and it is recommended in the official Angular documentation.

Mocha

  • Flexibility - Mocha is a little harder to set up and divided into more libraries but it is more flexible and open to extensions.
  • Community - has many plugins and extension to test unique scenarios.
  • Extensibility - very extensible, to the point where plugins, extensions and libraries are designed only to run on top of it.
  • Globals - creates test structure globals by default, but obviously not assertions, spies and mocks like Jasmine.

Ava

  • Ready-To-Go - comes with everything you need to start testing (besides spying and dubbing). Uses the syntax for test structure and assertions, and runs in Node.js.
  • Globals - it does not create any test globals, so you have more control over your tests.
  • Simplicity - simple structure and assertions without a complex API while supporting many advanced features.
  • Development - Ava only updates the files updated so tests will run fast in watch mode.
  • Speed - Runs tests in parallel as separate Node.js processes.

Here we can see some statistics taken from https://www.npmtrends.com/ and https://bundlephobia.com/.

1

2

Taking into account the data mentioned above we can opt Jest Framework as a test runner.

However, considering the nature of the project, namely that we use functional component approach (hooks etc.), we need some additional mechanism, React Test Library -test util, which helps us to make appropriate tests within the project environment.

Also, we should use some assertion and mocking library:

  • @testing-library/jest-dom library - a provider of a set of custom jest matchers that you can use to extend jest. These will make your tests more declarative, clear to read and to maintain.

  • Mock Service Worker - an API mocking library that uses Service Worker API to intercept actual requests.

Testing Library

Simple and complete testing utilities that encourages good testing practices. The library provides special tools for different frameworks like React. The most famous of them is React Testing Library which is very widely adopted. It is not focused on the implementation details of the component. React Testing Library uses the render logic to run our assertions. The testing is done from user’s perspective. That’s why this library doesn’t provide us the access to the component’s properties such as its state and props. The assertions are made from the DOM elements which can be accessed by the utility provided by React Testing Library.

Below, we can see distinguishing features of the test util

  • GitHub: ★ 19 500
  • Approach: testing the component from user’s perspective. If it relates to rendering components, it deals with DOM nodes rather than component instances, nor should it encourage dealing with component instances.
  • Shallow or deep rendering: testing using the DOM. There is no shallow or deep rendering.
  • Way of operation: enforces us to test the component according to the DOM and doesn’t provide us any means to gain access to the internals of the component.
  • Setup: it can be directly installed.
  • Support: has rich documentation and use cases available in the React community.

Examples

Further, we are going to review React Testing Library deeper with some simple testing examples.

RangeCounter component that represents two control buttons (for increment and decrement) and the current count value in between those buttons. We have props passed to the component (min and max). When we reach any of the range limit, we should see an alert message.

export const RangeCounter = props => {
  const { max, min } = props;
  const [counter, setCounter] = useState(min);
  const [hasEdited, setHasEdited] = useState(false);

  useEffect(() => {
    if (counter !== min && !hasEdited) {
      setHasEdited(true);
    }
  }, [counter]);

  return (
    <div className="RangeCounter">
      <span className="RangeCounter__title">Functional RangeCounter</span>
      <div className="RangeCounter__controls">
        <button
          disabled={counter <= min}
          onClick={() => setCounter(counter => counter - 1)}
        >
          -
        </button>
        <span data-testid="counter-value">{counter}</span>
        <button
          disabled={counter >= max}
          onClick={() => setCounter(counter => counter + 1)}
       >
         +
        </button>
      </div>
      {(counter >= max || counter <= min) && hasEdited && (
        <span className="RangeCounter__alert">Range limit reached!</span>
      )}
    </div>
  );
};

RangeCounter.defaultProps = {
min: 0,
max: 10
};

We will be testing a few scenarios

Testing that a user is able to increment when incrementing is allowed.

describe("when incrementing counter is allowed", () => {
    it("updates the counter value", async () => {
      const { getByTestId, getByText } = render(<RangeCounter min={2} />);
      const incrementButton = getByText("+");
      fireEvent.click(incrementButton);
      expect(getByTestId("counter-value").innerHTML).toEqual("3");
    });
});

The idea of this test is to check what is showing in the UI. That is done by getting the actual DOM element and checking its content, which represents what the user actually sees.

Testing that a user is not able to increment when count reaches maximum.

describe("when incrementing counter is not allowed", () => {
    it("does not update the counter value", async () => {
      const { getByTestId, getByText } = render(
        <RangeCounter min={0} max={0} />
      );
      const incrementButton = getByText("+");
      fireEvent.click(incrementButton);
      expect(getByTestId("counter-value").innerHTML).toEqual("0");
    });
});

The idea is that you search directly by the actual text that the user sees without the overhead work of finding the element that contains that text.


Useful links