Jest: The Ultimate Testing Framework for JavaScript Applications

Jest is a popular open-source JavaScript testing framework maintained by Facebook. It is primarily used for testing JavaScript applications, including client-side (frontend) and server-side (backend) code. Jest is particularly known for its ease of use, powerful features, and comprehensive testing capabilities.

You can do the following kinds of tests with Jest

  • Unit tests: Unit tests are used to test individual units of code, such as functions or methods.
  • Integration tests: Integration tests are used to test how different units of code interact with each other.
  • End-to-end tests: End-to-end tests are used to test the entire application from start to finish.
  • Snapshot tests: Snapshot tests are used to compare the output of a function or component to a saved snapshot.
  • Mocking: Mocking is used to simulate the behavior of external dependencies in tests.

The App we will be testing

In this post, I will be using a basic calculator app, which is a React application for unit testing. We are not going to focus on the code of this app as it is not the scope of this post. I used the create-react-app to set up this React app. The great news is you do not need to worry about configuring Jest because create-react-app does that job for you when you create the React app.

The following is the code for App.js. ( If you are interested, check for CSS used in App.css in the resources )

import React, { useState } from 'react';
import './App.css';

const App = () => {
  const [input, setInput] = useState('');
  const [result, setResult] = useState('');

  const handleButtonClick = (value) => {
    if (value === '=') {
      try {
        const result = calculateResult(input);
        setResult(result.toString());
      } catch (error) {
        setResult('Error');
      }
    } else if (value === 'C') {
      setInput('');
      setResult('');
    } else {
      setInput(input + value);
    }
  };

  const calculateResult = (inputExpression) => {
    const operations = inputExpression.match(/[+\-*/]/g);
    const operands = inputExpression.split(/[+\-*/]/).map(parseFloat);

    if (!operations || !operands) {
      throw new Error('Invalid expression');
    }

    let result = operands[0];
    for (let i = 0; i < operations.length; i++) {
      const operation = operations[i];
      const operand = operands[i + 1];

      switch (operation) {
        case '+':
          result += operand;
          break;
        case '-':
          result -= operand;
          break;
        case '*':
          result *= operand;
          break;
        case '/':
          result /= operand;
          break;
        default:
          throw new Error('Invalid operation');
      }
    }

    return result;
  };

  return (
    <div className="calculator"> 
      <div className="display">
        <input type="text" value={input} readOnly />
        <div className="result" data-testid="result">{result}</div> {/** data-testid is for testing purpose**/}
      </div>
      <div className="buttons">
        <button onClick={() => handleButtonClick('7')}>7</button>
        <button onClick={() => handleButtonClick('8')}>8</button>
        <button onClick={() => handleButtonClick('9')}>9</button>
        <button onClick={() => handleButtonClick('/')}>/</button>
        <button onClick={() => handleButtonClick('4')}>4</button>
        <button onClick={() => handleButtonClick('5')}>5</button>
        <button onClick={() => handleButtonClick('6')}>6</button>
        <button onClick={() => handleButtonClick('*')}>*</button>
        <button onClick={() => handleButtonClick('1')}>1</button>
        <button onClick={() => handleButtonClick('2')}>2</button>
        <button onClick={() => handleButtonClick('3')}>3</button>
        <button onClick={() => handleButtonClick('-')}>-</button>
        <button onClick={() => handleButtonClick('0')}>0</button>
        <button onClick={() => handleButtonClick('.')}>.</button>
        <button onClick={() => handleButtonClick('=')}>=</button>
        <button onClick={() => handleButtonClick('+')}>+</button>
        <button onClick={() => handleButtonClick('C')}>C</button>
      </div>
    </div>
  );
};

export default App;

Unit testing with Jest

When you need to run tests with Jest, you need to create a file/files with the following name conventions.

  • Files with .test.js suffix.
  • Files with .spec.js suffix.

Another way to do this is by creating a __tests__ folder, and then creating your test files inside this folder with .js extension.

If you use create-react-app to create, it automatically creates a test file for App.js , which is App.test.js file.

We will replace the Jest code App.test.js. But, first have a high-level look at what tests do.

Let’s take a look at some of the common components of the Jest framework that we are going to use in this post.

Globals in Jest

Globals in Jest are methods and objects that are available globally in the test environment. In Your test files, therefore, you do not need to add import or require statements ( as we usually include at the top of the file ). These globals make writing tests in Jest more convenient and help you in structuring test suites, making assertions, and setting up/tearing down common resources easily.

Jest provides several global functions and objects that you can use in your test files without any additional setup. Some of the most commonly used ones are in the following table. Please refer to the Jest API Guide to learn how to use these Globals.

describeDefines a test suite.
testDefines a single test.
expectUsed to make assertions about the output of a test.
beforeAllRuns a function before any of the tests in this file run.
beforeEachRuns a function before each test in this file runs.
afterAllRuns a function after all of the tests in this file run.
afterEachRuns a function after each test in this file runs.
timeoutSpecifies the maximum amount of time that a test is allowed to run before it is considered a failure.
concurrentRuns multiple tests concurrently.
onlyMarks a test as important and only runs it once.
skipMarks a test as skipped and does not run it.
todoMarks a test as a TODO and does not run it.

Matchers

Matchers are functions in Jest provided that allow you to make assertions in your test cases. Matchers are used to compare the actual value of an expression to an expected value and determine whether the test passes or fails.

In Jest, the Matchers are primarily used with the expect function to make assertions in test cases. For example, if you use toBe() Matcher with expect(), you would use it as follows:

expect(actualValue).toBe(expectedValue)

some of the common Matchers are:

  • toBe – Asserts that the value of a variable or expression is equal to a specific value.
  • toEqual – Asserts that the value of a variable or expression is equal to another variable or expression.
  • toBeTruthy – Asserts that a value is truthy.
  • toBeFalsy – Asserts that a value is falsy.
  • toHaveProperty – Asserts that a variable or expression has a specific property.
  • toMatch – Asserts that a value matches a specific regular expression.

For more detail check the Jest API reference on Matchers.

The Matchers are not limited to the Jest testing framework. They are also available in RTL, especially in @testing-library/jest-dom library. Some of them are

  • toBeInTheDocument: Checks if an element is present in the document.
  • toHaveTextContent: Checks if the text content of the element matches the expected value.
  • toBeVisible: Checks if an element is currently visible (not hidden or outside the viewport).
  • toBeDisabled: Checks if a form element or button is disabled.
  • toHaveAttribute: Checks if an element has a specific attribute with a certain value.
  • toHaveStyle: Checks if an element has a specific style applied to it.
  • toHaveClass: Checks if an element has a specific CSS class.

The good news is that you can access these Matchers in @testing-library/jest-dom via the expect() method in Jest.

UI testing with React Testing Library

While we mainly use Jest to test the functionality of the app, we will also be using React Testing Library (RTL) to render the app and simulate user interactions. RTL is a library of utilities that make it easy to write user-focused tests for React components. It tests the user interface of the component, rather than the implementation details.

**Please note, in this post my focus is not on RTL.**

import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import App from './App';

test('calculator app renders properly', () => {
  render(<App />);

  // Check if the calculator display is present
  const calculatorDisplay = screen.getByRole('textbox');
  expect(calculatorDisplay).toBeInTheDocument();

  // Check if the buttons are present
  const buttons = screen.getAllByRole('button');
  expect(buttons.length).toBeGreaterThan(0);

  // Check the initial state of the calculator
  expect(calculatorDisplay.value).toBe('');

  // Verify that the result is not displayed initially
  const resultElement = screen.queryByTestId('result');
  expect(resultElement).toBeEmptyDOMElement();

  // Optionally, you can check if specific buttons are present
  expect(screen.getByText('0')).toBeInTheDocument();
  expect(screen.getByText('1')).toBeInTheDocument();
  expect(screen.getByText('2')).toBeInTheDocument();
  // ... and so on for all other buttons

  // Optionally, you can check if the display is visible
  expect(calculatorDisplay).toBeVisible();
});

While we use RTL to test the UI components, we use the expect() method of Jest for assertions. Basically, we use both these frameworks side-by-side.

Note: I have added data-testid="result" in the div element that displays the result ( check the code in App.js above ).

Testing functionality of the app with Jest

RTL does not check the functionality of the calculator. For that, you need to use Jest. There are five tests we can perform to test the functionality of this calculator( addition, subtraction, multiplication, division, and Invalid Expressions ) of this calculator app.

Although I mentioned five tests, you can come up with more, especially to test use cases with regard to error handling. I will show you how to do theaddition and theInvalid Expression. The rest of the things, I will leave for you, as an exercise. You can also check how I did the test for division if you need further help.

Test for addition ( ex: 2+3=5 )

test('performs addition', () => {
  render(<App />);

  const inputElement = screen.getByRole('textbox');
   // Check the value of the textbox (initially it should be an empty string)
  expect(inputElement.value).toBe('');

  // Simulate clicking the buttons to enter the expression "2+3" into the calculator
  fireEvent.click(screen.getByText('2'));
  expect( inputElement.value ).toBe("2")
  // expect(inputElement.textContent).toBe(' 2');

  fireEvent.click(screen.getByText('+'));
  expect(inputElement.value).toBe('2+');

  fireEvent.click(screen.getByText('3'));
  expect(inputElement.value).toBe('2+3');


  // Simulate clicking the "=" button
  fireEvent.click(screen.getByText('='));

  //select the 'result' div and check if it's value is 5
  const resultElement = screen.getByTestId('result');
  expect(resultElement.textContent).toBe('5');//or expect(resultElement).toHaveTextContent('5')
});

Test for an invalid expression ( ex: 1+=)

test('handles error for invalid expression', () => {
  render(<App />);

  const inputElement = screen.getByRole('textbox');

  fireEvent.click(screen.getByText('1'));
  expect( inputElement.value ).toBe("1")

  fireEvent.click(screen.getByText('+'));
  expect( inputElement.value ).toBe("1+")

  fireEvent.click(screen.getByText('='));


  const resultElement = screen.getByTestId('result'); // Using getByTestId to target the result element
  expect(resultElement).toHaveTextContent('NaN'); // Using toHaveTextContent to check the content

});

In the code above, there are functions from both RTL and Jest. But, As mentioned above, we used Jest to test the functionality of the component we are testing. Here the expect(), which is called a global method in Jest, provides access to toBe Matcher, which can be used to check primitive values.

Conclusion

In a nutshell, Jest is a powerful and user-friendly testing framework for JavaScript applications. It simplifies testing with its easy-to-use API and built-in Matchers. On the other hand, React Testing Library (RTL) takes a user-centric approach, making sure your components behave as users expect.

Jest and RTL make a perfect team. They work side by side, complementing each other’s strengths. With Jest’s simplicity and RTL’s user-oriented testing, you can write comprehensive test suites that catch bugs effortlessly.

So, whether you’re a seasoned developer or just starting, Jest and RTL will empower you to write reliable tests and build outstanding JavaScript applications. Happy testing!

Resources

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top