TDD in React: From Beginner to Advanced ๐Ÿ”ฅ

Introdution

Test Driven Development (TDD) is a practice where you write tests before implementing functionality. In React projects, TDD ensures component reliability, promotes clean code, and builds confidence when refactoring. This article takes you from basic concepts to advanced strategies, complete with step-by-step examples, a comparison of benefits and drawbacks, and suggested hashtags for sharing your journey.


Tools and Setup๐Ÿ› ๏ธ

To get started youโ€™ll need:

  • Node.js and npm or yarn

  • Create React App or Vite for project scaffolding

  • Jest as the test runner

  • React Testing Library for user-focused component tests

  • msw (Mock Service Worker) to mock APIs

  • Cypress (optional) for end-to-end testing

npx create-react-app my-app 
cd my-app 
npm install --save-dev jest @testing-library/react @testing-library/jest-dom msw

Add to package.json:

"scripts": { "test": "react-scripts test --env=jsdom --watchAll=false --coverage" }

The Red โ†’ Green โ†’ Refactor Cycle ๐Ÿ”

  1. Red: Write a failing test.

  2. Green: Write the minimal code to make the test pass.

  3. Refactor: Clean up the code while keeping all tests green.

This loop enforces small steps, immediate feedback, and continuous confidence


Level 1: Basic ๐Ÿ”ฐ

Example: Text Component

  1. Write the test:
// src/tests/Title.test.js 
import { render, screen } from '@testing-library/react'; 
import Title from '../Title';

test('displays the correct text', () => {
  render(<Title text="Hello, React!" />);
  expect(screen.getByText('Hello, React!')).toBeInTheDocument();
});
  1. Implement the component:
// src/Title.js
const Title = ({ text }) => <h1>{text}</h1>;
export default Title;

Tips

  • Use accessible selectors (getByRole, getByText).

  • Keep tests focused on behavior, not implementation details.


Level 2: Intermediate โš™๏ธ

Complete Example: Login Form with State and Callbacks

  1. Write the test:
// src/__tests__/LoginForm.test.js
import { render, fireEvent, screen } from '@testing-library/react';
import LoginForm from '../LoginForm';

test('submits the correct data on submit', () => {
  const handleSubmit = jest.fn();
  render(<LoginForm onSubmit={handleSubmit} />);

  fireEvent.change(screen.getByLabelText('Email'), {
    target: { value: 'user@example.com' }
  });
  fireEvent.change(screen.getByLabelText('Password'), {
    target: { value: '123456' }
  });
  fireEvent.click(screen.getByRole('button', { name: 'Login' }));

  expect(handleSubmit).toHaveBeenCalledWith({
    email: 'user@example.com',
    password: '123456'
  });
});
  1. Implement the form with state and callbacks:
// src/LoginForm.js
import { useState } from 'react';

function LoginForm({ onSubmit }) {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = e => {
    e.preventDefault();
    onSubmit({ email, password });
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Email
        <input
          type="email"
          value={email}
          onChange={e => setEmail(e.target.value)}
        />
      </label>
      <label>
        Password
        <input
          type="password"
          value={password}
          onChange={e => setPassword(e.target.value)}
        />
      </label>
      <button type="submit">Login</button>
    </form>
  );
}

export default LoginForm;

Best Practices

  • Separate presentation from state logic.

  • Simulate only user behavior, not internal state.


Level 3: Advanced ๐Ÿš€

Explore code examples for each test type in a real-world context.

๐Ÿงฎ 1. Unit Test

Focus on pure functions and isolated components.

// src/__tests__/sum.test.js
import sum from '../sum';

test('adds two numbers', () => {
  expect(sum(2, 3)).toBe(5);
});
// src/sum.js
export default function sum(a, b) {
  return a + b;
}

๐Ÿ”— 2. Integration Test

Render multiple components together and verify interactions.

// src/__tests__/Dashboard.test.js
import { render, screen } from '@testing-library/react';
import Dashboard from '../Dashboard';
import { UserProvider } from '../UserContext';

test('displays welcome message after login', () => {
  render(
    <UserProvider value={{ name: 'Maria' }}>
      <Dashboard />
    </UserProvider>
  );
  expect(screen.getByText('Welcome, Maria')).toBeInTheDocument();
});

๐Ÿช 3. Hook Test

Use renderHook to test custom hooks.

// src/__tests__/useCounter.test.js
import { renderHook, act } from '@testing-library/react';
import useCounter from '../useCounter';

test('increments and decrements correctly', () => {
  const { result } = renderHook(() => useCounter(10));

  act(() => {
    result.current.increment();
    result.current.decrement();
  });

  expect(result.current.count).toBe(10);
});
// src/useCounter.js
import { useState } from 'react';

export default function useCounter(initial) {
  const [count, setCount] = useState(initial);
  return {
    count,
    increment: () => setCount(c => c + 1),
    decrement: () => setCount(c => c - 1)
  };
}

๐ŸŒ 4. End-to-End Test (Cypress)

Simulate a real user flow in the browser

// cypress/integration/login.spec.js
describe('Login Flow', () => {
  it('logs in successfully', () => {
    cy.visit('http://localhost:3000/login');
    cy.get('input[type="email"]').type('user@example.com');
    cy.get('input[type="password"]').type('123456');
    cy.contains('Login').click();
    cy.contains('Dashboard').should('be.visible');
  });
});

Benefits and Drawbacks

Simple intensity graph:

Benefits            |โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
Drawbacks           |โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ

Conclusion ๐ŸŽฏ

Adopting TDD in React requires discipline up front but pays off with higher quality, maintainability, and confidence. Start with simple tests, move to forms with state and callbacks, then explore integration, hook, and end-to-end tests. Over time, TDD becomes a natural part of your workflow and transforms how you build UIs

#react #tdd #testdrivendevelopment #frontend #javascript #qualitycode #webdevelopment #testing #jest #cypress

0
Subscribe to my newsletter

Read articles from Johnny Hideki Kinoshita de Faria directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Johnny Hideki Kinoshita de Faria
Johnny Hideki Kinoshita de Faria

Technology professional with over 15 years of experience delivering innovative, scalable, and secure solutions โ€” especially within the financial sector. I bring deep expertise in Oracle PL/SQL (9+ years), designing robust data architectures that ensure performance and reliability. On the back-end side, Iโ€™ve spent 6 years building enterprise-grade applications using .NET, applying best practices like TDD and clean code to deliver high-quality solutions. In addition to my backend strengths, I have 6 years of experience with PHP and JavaScript, allowing me to develop full-stack web applications that combine strong performance with intuitive user interfaces. I've led and contributed to projects involving digital account management, integration of VISA credit and debit transactions, modernization of payment systems, financial analysis tools, and fraud prevention strategies. Academically, I hold a postgraduate certificate in .NET Architecture and an MBA in IT Project Management, blending technical skill with business acumen. Over the past 6 years, Iโ€™ve also taken on leadership roles โ€” managing teams, mentoring developers, and driving strategic initiatives. I'm fluent in agile methodologies and make consistent use of tools like Azure Boards to coordinate tasks and align team performance with delivery goals.