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 ๐
Red: Write a failing test.
Green: Write the minimal code to make the test pass.
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
- 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();
});
- 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
- 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'
});
});
- 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
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.