Why Most Tests Are Worthless
The biggest testing mistake: testing implementation details. Tests that verify "the button has className 'btn-primary'" or "the component called setState 3 times" break on every refactor without catching any real bugs. They increase maintenance cost without increasing confidence.
Good tests verify BEHAVIOR from the user's perspective: "when I click submit, the success message appears." These tests survive refactors because you can completely rewrite the internal implementation and the test still passes — if the behavior is correct.
The testing philosophy: "Write tests. Not too many. Mostly integration." — Guillermo Rauch (Vercel CEO). Focus on integration tests that verify real user flows, supplement with unit tests for complex logic, and use E2E tests sparingly for critical paths.
Key Takeaways
Vitest 3: The Modern Test Runner
Vitest 3 is the standard test runner for modern JavaScript projects. It is Jest-compatible (same API: describe, it, expect) but 10-20x faster because it uses Vite's instant HMR and ESM support. No more waiting 30 seconds for tests to start.
Vitest provides: instant watch mode (only re-runs affected tests), in-source testing (tests alongside your code), browser mode (run tests in a real browser), TypeScript support (zero config), and code coverage (v8 or Istanbul).
// vitest.config.ts import { defineConfig } from 'vitest/config'; import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [react()], test: { globals: true, // No need to import describe, it, expect environment: 'jsdom', // DOM environment for React setupFiles: './src/test/setup.ts', coverage: { provider: 'v8', reporter: ['text', 'html', 'lcov'], thresholds: { lines: 80, functions: 80, branches: 75, }, }, css: true, // Process CSS imports }, }); // src/test/setup.ts import '@testing-library/jest-dom/vitest'; // Basic test example import { describe, it, expect } from 'vitest'; import { formatCurrency, calculateDiscount } from './utils'; describe('formatCurrency', () => { it('formats USD amount with two decimals', () => { expect(formatCurrency(99.9, 'USD')).toBe('$99.90'); }); it('handles zero correctly', () => { expect(formatCurrency(0, 'USD')).toBe('$0.00'); }); it('formats INR with proper symbol', () => { expect(formatCurrency(1500, 'INR')).toBe('₹1,500.00'); }); }); describe('calculateDiscount', () => { it('applies percentage discount', () => { expect(calculateDiscount(100, { type: 'percentage', value: 20 })).toBe(80); }); it('never goes below zero', () => { expect(calculateDiscount(10, { type: 'fixed', value: 50 })).toBe(0); }); });
Key Takeaways
React Testing Library: User-Centric Testing
React Testing Library (RTL) enforces good testing practices by design. Its API forces you to find elements the way users do: by visible text, labels, roles, and placeholders — not by CSS class names, data-testids, or component internals.
The priority order for queries: getByRole > getByLabelText > getByText > getByPlaceholderText > getByTestId. If you find yourself reaching for getByTestId frequently, it usually means your HTML lacks proper accessibility attributes.
RTL's core philosophy: "The more your tests resemble the way your software is used, the more confidence they can give you."
import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { LoginForm } from './LoginForm'; describe('LoginForm', () => { it('submits login credentials and shows success', async () => { const user = userEvent.setup(); const mockLogin = vi.fn().mockResolvedValue({ success: true }); render(<LoginForm onLogin={mockLogin} />); // Query by accessible role and label — like a real user await user.type(screen.getByLabelText(/email/i), 'ash@inkandhorizon.com'); await user.type(screen.getByLabelText(/password/i), 'securePassword123'); await user.click(screen.getByRole('button', { name: /sign in/i })); // Assert behavior, not implementation await waitFor(() => { expect(screen.getByText(/welcome back/i)).toBeInTheDocument(); }); expect(mockLogin).toHaveBeenCalledWith({ email: 'ash@inkandhorizon.com', password: 'securePassword123', }); }); it('shows validation error for invalid email', async () => { const user = userEvent.setup(); render(<LoginForm onLogin={vi.fn()} />); await user.type(screen.getByLabelText(/email/i), 'not-an-email'); await user.click(screen.getByRole('button', { name: /sign in/i })); expect(screen.getByText(/please enter a valid email/i)).toBeInTheDocument(); }); it('disables submit button while loading', async () => { const user = userEvent.setup(); const slowLogin = vi.fn(() => new Promise(r => setTimeout(r, 1000))); render(<LoginForm onLogin={slowLogin} />); await user.type(screen.getByLabelText(/email/i), 'ash@ih.com'); await user.type(screen.getByLabelText(/password/i), 'pass123'); await user.click(screen.getByRole('button', { name: /sign in/i })); expect(screen.getByRole('button', { name: /signing in/i })).toBeDisabled(); }); });
Key Takeaways
Mocking Patterns: APIs, Modules & Timers
Mocking isolates the unit under test from external dependencies. The three essential patterns: mock API calls (prevent network requests in tests), mock modules (replace heavy imports), and mock timers (control setTimeout/setInterval).
MSW (Mock Service Worker) is the recommended approach for API mocking — it intercepts network requests at the service worker level, providing realistic behavior without modifying application code.
// Mock API calls with MSW (Mock Service Worker) import { http, HttpResponse } from 'msw'; import { setupServer } from 'msw/node'; const server = setupServer( http.get('/api/users/me', () => { return HttpResponse.json({ id: 'user_123', name: 'Ashutosh', role: 'admin', }); }), http.post('/api/posts', async ({ request }) => { const body = await request.json(); return HttpResponse.json({ id: 'post_456', ...body }, { status: 201 }); }) ); beforeAll(() => server.listen()); afterEach(() => server.resetHandlers()); afterAll(() => server.close()); // Mock timers describe('auto-save', () => { it('saves draft every 30 seconds', () => { vi.useFakeTimers(); const mockSave = vi.fn(); render(<Editor onAutoSave={mockSave} />); // Fast-forward 30 seconds vi.advanceTimersByTime(30000); expect(mockSave).toHaveBeenCalledTimes(1); vi.advanceTimersByTime(30000); expect(mockSave).toHaveBeenCalledTimes(2); vi.useRealTimers(); }); }); // Mock modules vi.mock('./analytics', () => ({ trackEvent: vi.fn(), trackPageView: vi.fn(), }));
Testing Hook and Custom Logic
Custom React hooks need special testing because they can only be called inside components. Use renderHook from @testing-library/react to test hooks in isolation. For complex logic that does not depend on React, extract it into pure functions and test those directly.
The testing strategy: extract business logic into pure functions (easy to test), keep hooks thin (bridge between React lifecycle and business logic), test complex hooks with renderHook, and test simple hooks indirectly through component tests.
import { renderHook, act, waitFor } from '@testing-library/react'; import { useDebounce } from './useDebounce'; describe('useDebounce', () => { it('debounces value updates', async () => { vi.useFakeTimers(); const { result, rerender } = renderHook( ({ value }) => useDebounce(value, 300), { initialProps: { value: '' } } ); // Initial value expect(result.current).toBe(''); // Update value — debounced value should NOT change yet rerender({ value: 'hello' }); expect(result.current).toBe(''); // Still empty // Fast-forward 300ms act(() => { vi.advanceTimersByTime(300); }); expect(result.current).toBe('hello'); // Now updated vi.useRealTimers(); }); }); // Pure function tests — no React needed import { validateEmail, parseQueryString, slugify } from './utils'; describe('validateEmail', () => { it.each([ ['ash@inkandhorizon.com', true], ['user@domain.co.in', true], ['invalid', false], ['@domain.com', false], ['user@', false], ])('validates %s → %s', (email, expected) => { expect(validateEmail(email)).toBe(expected); }); }); describe('slugify', () => { it.each([ ['Hello World', 'hello-world'], ['React 19 Server Components', 'react-19-server-components'], ['TypeScript & JavaScript', 'typescript-javascript'], ])('slugifies "%s" → "%s"', (input, expected) => { expect(slugify(input)).toBe(expected); }); });
Key Takeaways
Key Takeaways
Good tests verify BEHAVIOR, not implementation. Use React Testing Library to query elements like users do (by role, label, text). Use Vitest 3 for instant feedback with Jest-compatible APIs. Use MSW for realistic API mocking.
For interviews: explain the Testing Trophy (static > unit > integration > E2E), demonstrate Testing Library queries, discuss when to mock vs when to use real dependencies, and show how parameterized tests (it.each) reduce test code duplication.