DEV Community

Cover image for React Mastery Series – Day 26: React Testing – Unit Testing, Integration Testing & End-to-End Testing
Siva Samanthapudi
Siva Samanthapudi

Posted on

React Mastery Series – Day 26: React Testing – Unit Testing, Integration Testing & End-to-End Testing

Welcome back to the React Mastery Series!

In the previous article, we explored React Performance Optimization and learned techniques like:

  • Lazy Loading
  • Code Splitting
  • React.memo()
  • useMemo()
  • useCallback()
  • Virtualization
  • Performance monitoring

Today, we'll cover one of the most valuable skills for enterprise React developers:

React Testing

Many developers think testing is optional.

In reality, every production application needs automated tests to ensure that new features don't accidentally break existing functionality.

Testing helps teams:

  • Ship changes confidently
  • Catch bugs early
  • Improve code quality
  • Reduce regression issues
  • Enable safe refactoring

Why Testing Matters

Imagine an internet banking application.

A developer changes the login page.

Everything appears to work.

After deployment, users discover they can no longer transfer money because a shared component was unintentionally broken.

Without automated tests:

Developer Changes Code
          │
          ▼
Deploy to Production
          │
          ▼
Users Find Bugs
Enter fullscreen mode Exit fullscreen mode

With automated tests:

Developer Changes Code
          │
          ▼
Run Test Suite
          │
          ▼
Tests Fail
          │
          ▼
Fix Before Deployment
Enter fullscreen mode Exit fullscreen mode

Testing becomes a safety net.


Types of Testing

React applications typically use three levels of testing.

          E2E Tests
              ▲
              │
     Integration Tests
              ▲
              │
        Unit Tests
Enter fullscreen mode Exit fullscreen mode

Each level serves a different purpose.


Unit Testing

Unit tests verify one small piece of functionality.

Examples:

  • A Button component
  • A utility function
  • A custom hook
  • A reducer

Example:

function sum(a: number, b: number) {
  return a + b;
}
Enter fullscreen mode Exit fullscreen mode

Test:

test("adds two numbers", () => {
  expect(sum(2, 3)).toBe(5);
});
Enter fullscreen mode Exit fullscreen mode

Only one function is being tested.


Integration Testing

Integration tests verify multiple components working together.

Example:

Login Form
     │
     ▼
Validation
     │
     ▼
API Call
     │
     ▼
Dashboard
Enter fullscreen mode Exit fullscreen mode

Instead of testing each component separately, we verify the complete flow.


End-to-End (E2E) Testing

E2E testing simulates a real user.

Example:

Open Website
      │
      ▼
Enter Username
      │
      ▼
Enter Password
      │
      ▼
Click Login
      │
      ▼
Dashboard Appears
Enter fullscreen mode Exit fullscreen mode

These tests verify that the application works from the user's perspective.


Testing Tools

Modern React projects commonly use:

Purpose Tool
Test Runner Jest or Vitest
Component Testing React Testing Library
Browser Automation Playwright or Cypress

If you're using Vite, many teams prefer Vitest because it's optimized for the Vite ecosystem.


Testing Philosophy

React Testing Library encourages this mindset:

Test your application the way users use it.

Instead of checking implementation details:

❌ Internal state

✅ Visible UI

❌ Private methods

✅ User interactions

This results in more reliable tests.


Installing Testing Libraries

For Jest:

npm install --save-dev jest
Enter fullscreen mode Exit fullscreen mode

For React Testing Library:

npm install --save-dev @testing-library/react
npm install --save-dev @testing-library/jest-dom
npm install --save-dev @testing-library/user-event
Enter fullscreen mode Exit fullscreen mode

Your First Component

type ButtonProps = {
  text: string;
};

function Button({ text }: ButtonProps) {
  return <button>{text}</button>;
}

export default Button;
Enter fullscreen mode Exit fullscreen mode

Testing a Component

import { render, screen } from "@testing-library/react";

import Button from "./Button";

test("renders button text", () => {
  render(<Button text="Save" />);

  expect(
    screen.getByText("Save")
  ).toBeInTheDocument();
});
Enter fullscreen mode Exit fullscreen mode

Flow:

Render Component
        │
        ▼
Find Element
        │
        ▼
Verify Output
Enter fullscreen mode Exit fullscreen mode

Understanding render()

render() mounts a React component into a virtual DOM for testing.

render(<Login />);
Enter fullscreen mode Exit fullscreen mode

Now the test can interact with the component just like a browser would.


Finding Elements

React Testing Library provides multiple queries.

screen.getByText("Login");
Enter fullscreen mode Exit fullscreen mode
screen.getByRole("button");
Enter fullscreen mode Exit fullscreen mode
screen.getByPlaceholderText("Email");
Enter fullscreen mode Exit fullscreen mode

Prefer queries based on accessibility, such as getByRole(), because they closely match how assistive technologies interact with your application.


Testing User Interaction

Example component:

import { useState } from "react";

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <>
      <h2>{count}</h2>

      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
    </>
  );
}

export default Counter;
Enter fullscreen mode Exit fullscreen mode

Test:

import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

test("increments counter", async () => {
  render(<Counter />);

  await userEvent.click(
    screen.getByRole("button", {
      name: /increment/i,
    })
  );

  expect(screen.getByText("1")).toBeInTheDocument();
});
Enter fullscreen mode Exit fullscreen mode

The test behaves like a real user clicking the button.


Testing Forms

Example:

render(<LoginForm />);

await userEvent.type(
  screen.getByLabelText(/email/i),
  "user@example.com"
);

await userEvent.type(
  screen.getByLabelText(/password/i),
  "password123"
);

await userEvent.click(
  screen.getByRole("button", {
    name: /login/i,
  })
);
Enter fullscreen mode Exit fullscreen mode

This closely mirrors how users interact with forms.


Mocking API Calls

Components often depend on backend APIs.

Instead of making real network requests during tests, mock them.

Example:

jest.mock("../services/userService", () => ({
  getUsers: jest.fn(),
}));
Enter fullscreen mode Exit fullscreen mode

Now you control the API response without depending on a server.

Benefits:

  • Faster tests
  • Reliable execution
  • No internet dependency

Testing Loading States

Example component:

if (loading) {
  return <Spinner />;
}
Enter fullscreen mode Exit fullscreen mode

Test:

expect(
  screen.getByRole("status")
).toBeInTheDocument();
Enter fullscreen mode Exit fullscreen mode

Verify that users see a loading indicator while data is being fetched.


Testing Error States

Example:

render(<ErrorMessage message="Something went wrong" />);

expect(
  screen.getByText("Something went wrong")
).toBeInTheDocument();
Enter fullscreen mode Exit fullscreen mode

Always test both success and failure scenarios.


Testing Redux Components

When testing components connected to Redux, wrap them with a Provider.

import { Provider } from "react-redux";

render(
  <Provider store={store}>
    <Dashboard />
  </Provider>
);
Enter fullscreen mode Exit fullscreen mode

This provides access to the Redux Store during testing.


Testing React Router

Components using React Router should be wrapped in a router.

import { MemoryRouter } from "react-router-dom";

render(
  <MemoryRouter>
    <Profile />
  </MemoryRouter>
);
Enter fullscreen mode Exit fullscreen mode

MemoryRouter is designed specifically for testing navigation.


End-to-End Testing with Playwright

Example:

import { test, expect } from "@playwright/test";

test("user can log in", async ({ page }) => {
  await page.goto("http://localhost:5173");

  await page.fill("#email", "user@example.com");
  await page.fill("#password", "password123");

  await page.click("button[type='submit']");

  await expect(page).toHaveURL("/dashboard");
});
Enter fullscreen mode Exit fullscreen mode

This launches a real browser and verifies the entire login flow.


Enterprise Testing Strategy

A typical enterprise testing pyramid looks like this:

              Few

        End-to-End Tests

               ▲

      Integration Tests

               ▲

      Many Unit Tests
Enter fullscreen mode Exit fullscreen mode

Why?

  • Unit tests are fast.
  • Integration tests verify workflows.
  • E2E tests validate critical user journeys.

Keeping this balance provides broad coverage without slowing development.


Folder Structure

A scalable project structure:

src
├── components
│   ├── Button.tsx
│   └── Button.test.tsx
├── hooks
│   ├── useAuth.ts
│   └── useAuth.test.ts
├── services
│   ├── userService.ts
│   └── userService.test.ts
Enter fullscreen mode Exit fullscreen mode

Keep tests close to the code they validate.


Common Mistakes

1. Testing Implementation Details

Avoid checking internal state or private functions.

Instead, verify what users can see and do.


2. Ignoring Edge Cases

Test:

  • Empty data
  • Loading states
  • Error responses
  • Invalid input
  • Unauthorized users

3. Overusing End-to-End Tests

E2E tests are valuable but slower than unit and integration tests.

Reserve them for critical user journeys.


4. Skipping Accessibility Queries

Prefer:

screen.getByRole("button", { name: /save/i,});
Enter fullscreen mode Exit fullscreen mode

Instead of relying on CSS classes or IDs whenever possible.


Best Practices

  • Write tests alongside new features.
  • Test behavior instead of implementation.
  • Mock external services.
  • Cover success and failure scenarios.
  • Keep tests independent.
  • Use meaningful test names.
  • Run tests in your CI/CD pipeline before deployment.

Key Takeaways

Today, we learned:

✅ Unit tests verify individual components and functions.
✅ Integration tests validate multiple components working together.
✅ End-to-End tests simulate real user behavior.
✅ React Testing Library focuses on user interactions.
✅ Mocking makes tests faster and more reliable.
✅ A balanced testing strategy improves confidence in production releases.


Coming Next 🚀

In Day 27, we will explore:

Authentication & Authorization in React – JWT, Protected Routes & Role-Based Access Control

We will learn:

  • Authentication vs Authorization
  • JWT (JSON Web Tokens)
  • Login and Logout flows
  • Token storage strategies
  • Refresh tokens
  • Protected routes
  • Role-Based Access Control (RBAC)
  • Enterprise authentication architecture

Authentication is one of the most important building blocks of secure React applications, and every frontend developer should understand how it works in production.

Happy Coding! 🚀

Top comments (0)