Unit Testing Components with Jest

Loading

Unit testing is essential for ensuring that your components work correctly and that your app’s behavior remains consistent over time. In React, Jest is one of the most popular testing frameworks, and it’s often used in conjunction with React Testing Library for testing React components. Jest helps in writing unit tests that verify the behavior of individual components, ensuring that they function correctly when integrated into larger applications.

1. Setting Up Jest and React Testing Library

If you are using Create React App (CRA), Jest and React Testing Library come pre-installed. However, if you’re setting up manually, you can install them by running:

npm install --save-dev jest @testing-library/react @testing-library/jest-dom

2. Basic Structure of Unit Tests in React

A unit test for a React component generally contains three main parts:

  1. Rendering the Component: You need to render the component into a test DOM.
  2. Interaction: Simulating events like clicks or user input.
  3. Assertions: Checking the expected behavior or result after the interaction.

3. Example: Simple React Component

Let’s create a simple React component that we will unit test.

Example Component: Button.js

import React, { useState } from 'react';

const Button = ({ initialText }) => {
  const [text, setText] = useState(initialText);

  const handleClick = () => {
    setText('Clicked');
  };

  return (
    <button onClick={handleClick}>
      {text}
    </button>
  );
};

export default Button;

This component displays a button with an initial label passed as a prop. When clicked, the label changes to “Clicked.”

4. Creating a Unit Test for the Component

Now, let’s write a unit test for this Button component. We will verify two things:

  1. The button initially displays the text passed as a prop.
  2. The button text changes to “Clicked” when clicked.

Test File: Button.test.js

import { render, screen, fireEvent } from '@testing-library/react';
import Button from './Button'; // Assuming Button is in the same folder

describe('Button Component', () => {
  test('displays initial text', () => {
    render(<Button initialText="Click Me" />);
    
    // Find the button element
    const button = screen.getByRole('button');
    
    // Assert the initial text is displayed correctly
    expect(button).toHaveTextContent('Click Me');
  });

  test('changes text on click', () => {
    render(<Button initialText="Click Me" />);
    
    const button = screen.getByRole('button');
    
    // Simulate a click event
    fireEvent.click(button);
    
    // Assert the text changes to 'Clicked'
    expect(button).toHaveTextContent('Clicked');
  });
});

5. Explanation of the Test Code

  • render(<Button initialText="Click Me" />): This renders the Button component with an initial prop initialText="Click Me".
  • screen.getByRole('button'): This gets the button element from the rendered output using the role="button" attribute.
  • fireEvent.click(button): This simulates a click event on the button.
  • expect(button).toHaveTextContent('Click Me'): This assertion checks that the button initially displays the text Click Me.
  • expect(button).toHaveTextContent('Clicked'): This checks that after clicking the button, its text changes to Clicked.

6. Running the Tests

Once you have written the test, you can run Jest using the following command:

npm test

Jest will look for files with the .test.js suffix and execute the tests.

7. Mocking Functions and Props

Sometimes, components depend on external functions or props. Jest allows you to mock those functions or props to isolate the component under test.

Example: Mocking a Callback Prop

Let’s modify the Button component so that it accepts a callback prop (onClickCallback), which is called when the button is clicked.

Modified Button.js:

import React, { useState } from 'react';

const Button = ({ initialText, onClickCallback }) => {
  const [text, setText] = useState(initialText);

  const handleClick = () => {
    setText('Clicked');
    if (onClickCallback) onClickCallback();
  };

  return (
    <button onClick={handleClick}>
      {text}
    </button>
  );
};

export default Button;

Now we will mock the onClickCallback function in our test to check if it was called.

Test File: Button.test.js with Mocked Function

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

describe('Button Component', () => {
  test('calls onClickCallback when clicked', () => {
    const mockCallback = jest.fn();
    render(<Button initialText="Click Me" onClickCallback={mockCallback} />);
    
    const button = screen.getByRole('button');
    
    // Simulate click event
    fireEvent.click(button);
    
    // Assert that the callback was called once
    expect(mockCallback).toHaveBeenCalledTimes(1);
  });
});

Explanation:

  • jest.fn(): Creates a mock function.
  • expect(mockCallback).toHaveBeenCalledTimes(1): Checks that the callback function was called exactly once after the button was clicked.

8. Testing Component Lifecycle Methods with useEffect

If your component uses hooks like useEffect, you can test the lifecycle behavior as well.

Example: Testing useEffect with useState

Let’s modify the Button component to update its text when a timer expires.

import React, { useState, useEffect } from 'react';

const Button = ({ initialText }) => {
  const [text, setText] = useState(initialText);

  useEffect(() => {
    const timer = setTimeout(() => {
      setText('Timeout Reached');
    }, 2000);

    return () => clearTimeout(timer); // Cleanup timer on unmount
  }, []);

  return (
    <button>
      {text}
    </button>
  );
};

export default Button;

Test File: Button.test.js with Timer

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

jest.useFakeTimers(); // Mock the timer functions

describe('Button Component', () => {
  test('updates text after timer', () => {
    render(<Button initialText="Click Me" />);
    
    // Initially, the button should display "Click Me"
    const button = screen.getByRole('button');
    expect(button).toHaveTextContent('Click Me');
    
    // Fast-forward the timers
    jest.advanceTimersByTime(2000);
    
    // After 2 seconds, the text should update
    expect(button).toHaveTextContent('Timeout Reached');
  });
});

Explanation:

  • jest.useFakeTimers(): This tells Jest to mock the timer functions (setTimeout, setInterval, etc.).
  • jest.advanceTimersByTime(2000): This simulates the passage of 2 seconds, causing the setTimeout inside the useEffect hook to trigger.
  • expect(button).toHaveTextContent('Timeout Reached'): This checks that the text of the button has changed after the timer.

Leave a Reply

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