DEV Community

Cover image for Mocking, Parallelism, and Streamlining: Optimizing React Tests
Jamena McInteer
Jamena McInteer

Posted on

Mocking, Parallelism, and Streamlining: Optimizing React Tests

Introduction

As developers, we understand the importance of a fast and reliable test suite. Slow tests can significantly hinder productivity, and redundant or inefficient testing practices can lead to slow, flaky tests and frustration. Recently, I embarked on a journey to optimize the test performance in a React app, and I’m excited to share the technical strategies and insights that made a measurable impact.

In this post, I’ll detail the changes I implemented, including converting tests into unit tests, restructuring components, adding parallelism in CircleCI, mocking heavy components, and removing redundant tests.


1. Converting to Unit Tests

One of the first steps was converting some integration or end-to-end (E2E) tests into unit tests. While E2E tests are crucial for validating the complete functionality, we rely on Cypress to run E2E tests when pull requests are merged. This eliminates the need to write E2E tests in React Testing Library (RTL). Since tests in RTL are run by CircleCI for every commit, speed is more important than full end-to-end coverage.

Why Unit Tests?

Unit tests focus on individual functions or components in isolation, offering several advantages:

  • Speed: Since they run in isolation without the overhead of rendering an entire application or navigating through pages, they execute much faster.
  • Debuggability: Failures are easier to pinpoint, as unit tests target specific pieces of functionality.

Example

Before optimization, a test for a dashboard screen validated multiple charts, tables, and tabs. I broke down this screen into smaller components like Chart, Table, and SearchBar, each with its own dedicated unit tests:

// Before: Testing the entire dashboard
render(<Dashboard />);
expect(screen.getByText("Chart Title")).toBeInTheDocument();

// After: Unit test for Chart component
render(<Chart data={mockData} />);
expect(screen.getByText("Chart Title")).toBeInTheDocument();
Enter fullscreen mode Exit fullscreen mode

By testing components individually, I reduced the runtime and complexity of the test suite.


2. Component Restructuring for Isolated Tests

To enable better unit testing, I moved parts of complex screens into their own reusable components. This not only improved testability but also enhanced maintainability and readability of the codebase.

Example

A screen originally handled rendering bookmarks, search functionality, and large tables within tabs. After restructuring, these features were moved into separate components:

  • Bookmark
  • SearchBar
  • Tabs

Each component now has its own dedicated unit tests, reducing the reliance on broader integration tests.

// Unit test for SearchBar
render(<SearchBar onSearch={mockOnSearch} />);
fireEvent.change(screen.getByPlaceholderText("Search..."), {
  target: { value: "test" },
});
expect(mockOnSearch).toHaveBeenCalledWith("test");
Enter fullscreen mode Exit fullscreen mode

3. Parallelism in CircleCI

Test suites can grow large, and running them sequentially becomes a bottleneck. To address this, I enabled parallelism in CircleCI, allowing tests to be distributed across multiple containers.

Implementation

In the .circleci/config.yml file, I:

  • Added parallelism: 8 to the test job.
  • Sharded the test suite using Vitest's --shard command and environment variables (CIRCLE_NODE_INDEX and CIRCLE_NODE_TOTAL).
jobs:
  test:
    docker:
      - image: cimg/node:lts
    parallelism: 8
    steps:
      - checkout
      - run:
          name: Run Tests
          command: |
            yarn test:ci --shard $((CIRCLE_NODE_INDEX + 1))/$CIRCLE_NODE_TOTAL
Enter fullscreen mode Exit fullscreen mode

With this setup, the test suite was divided into smaller chunks, running simultaneously on different nodes. This change alone reduced test runtime by a significant amount.


4. Mocking Heavy Components

Some components, like charts or maps, are resource-intensive to render. While these components were crucial for the application, they weren’t always the focus of the tests.

Solution

I mocked these components in tests where they weren’t the subject of validation. For example, a Chart component was replaced with a simple mock:

import * as Chart from './components/charts'
vi.spyOn(Chart, 'default').mockImplementation(() => <div data-testId="mock-chart" />)

// Test
render(<Dashboard />);
expect(screen.getByTestId("mock-chart")).toBeInTheDocument();
Enter fullscreen mode Exit fullscreen mode

This approach ensured that the test suite remained fast without compromising on coverage.


5. Removing Redundant Tests

Another optimization was identifying and removing redundancy in the test suite. Previously, the same functionality was tested:

  • At the route level (e.g., /dashboard tests all components on the page).
  • Individually in component-level unit tests.

Streamlining Tests

By trusting the unit tests for component behavior, I simplified route-level tests to focus on integration scenarios, reducing duplication.


Results

These optimizations led to significant improvements:

  • Test runtime: Test step (including installing npm packages and linting) was reduced by 50% in CircleCI after introducing these changes. The tests themselves made even bigger gains, going from on average 7 min to under 3 min.

CircleCI test runtime results for before the changes showing total runtime of 9m 10s and test runtime of 7m 20s

CircleCI test runtime results for after the changes showing total runtime of 4m 34s (down 50%) and test runtime of 2m 49s

  • Reliability: Fewer flaky tests, thanks to mocking and isolated unit tests.
  • Maintainability: Simplified codebase with reusable components and streamlined tests.

Conclusion

Optimizing test performance isn’t just about speed—it’s about creating a testing strategy that’s reliable, maintainable, and scalable. By converting to unit tests, restructuring components, leveraging parallelism, mocking heavy components, and eliminating redundancy, I was able to achieve a faster, more efficient test suite.

If you’re facing similar challenges, I encourage you to explore these techniques in your projects. Feel free to share your experiences or ask questions in the comments!

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay