React Testing Library Best Practices

Comprehensive testing strategies with React Testing Library, Jest, and modern testing patterns

# React Testing Library Best Practices

Comprehensive guide for testing React applications with React Testing Library, focusing on user-centric testing patterns and best practices.

---

## Core Testing Principles

1. **Test User Behavior, Not Implementation**
   - Focus on what users see and interact with
   - Avoid testing internal component state or implementation details
   - Write tests that give confidence in real user interactions
   - Example user-focused tests:
     ```tsx
     import { render, screen, fireEvent, waitFor } from '@testing-library/react';
     import userEvent from '@testing-library/user-event';
     import { describe, it, expect, vi } from 'vitest';
     import LoginForm from './LoginForm';

     describe('LoginForm', () => {
       // Good: Tests user behavior
       it('allows user to login with valid credentials', async () => {
         const user = userEvent.setup();
         const mockOnLogin = vi.fn();

         render(<LoginForm onLogin={mockOnLogin} />);

         // Find elements by their accessible roles/labels
         const emailInput = screen.getByLabelText(/email/i);
         const passwordInput = screen.getByLabelText(/password/i);
         const loginButton = screen.getByRole('button', { name: /login/i });

         // Simulate user interactions
         await user.type(emailInput, 'user@example.com');
         await user.type(passwordInput, 'password123');
         await user.click(loginButton);

         // Assert on observable behavior
         expect(mockOnLogin).toHaveBeenCalledWith({
           email: 'user@example.com',
           password: 'password123'
         });
       });

       // Bad: Tests implementation details
       it('should not test internal state directly', () => {
         // Don't test useState, component methods, or internal state
         // These are implementation details that can change
       });
     });
     ```

2. **Query Priorities and Accessibility**
   - Use queries that mirror how users and assistive technologies find elements
   - Follow the query priority order: getByRole > getByLabelText > getByPlaceholderText > getByText > getByTestId
   - Ensure tests promote accessible markup
   - Example query usage:
     ```tsx
     import { render, screen } from '@testing-library/react';
     import ProductCard from './ProductCard';

     describe('ProductCard', () => {
       const mockProduct = {
         id: '1',
         name: 'Laptop',
         price: 999.99,
         description: 'High-performance laptop',
         inStock: true
       };

       it('displays product information accessibly', () => {
         render(<ProductCard product={mockProduct} />);

         // 1. getByRole - Most preferred
         const heading = screen.getByRole('heading', { name: 'Laptop' });
         const buyButton = screen.getByRole('button', { name: /add to cart/i });

         // 2. getByLabelText - For form elements
         const quantityInput = screen.getByLabelText(/quantity/i);

         // 3. getByText - For non-interactive content
         const price = screen.getByText('$999.99');

         // 4. getByTestId - Last resort
         const productContainer = screen.getByTestId('product-card');

         expect(heading).toBeInTheDocument();
         expect(buyButton).toBeEnabled();
         expect(quantityInput).toHaveValue(1);
         expect(price).toBeInTheDocument();
       });

       it('shows out of stock state correctly', () => {
         const outOfStockProduct = { ...mockProduct, inStock: false };
         render(<ProductCard product={outOfStockProduct} />);

         const buyButton = screen.getByRole('button', { name: /add to cart/i });
         expect(buyButton).toBeDisabled();

         // Check for accessible out of stock indication
         expect(screen.getByText(/out of stock/i)).toBeInTheDocument();
       });
     });
     ```

3. **Async Testing Patterns**
   - Use waitFor for elements that appear asynchronously
   - Use findBy queries for elements that will eventually appear
   - Handle loading states and async operations properly
   - Example async testing:
     ```tsx
     import { render, screen, waitFor } from '@testing-library/react';
     import userEvent from '@testing-library/user-event';
     import { rest } from 'msw';
     import { setupServer } from 'msw/node';
     import UserProfile from './UserProfile';

     // Setup Mock Service Worker
     const server = setupServer(
       rest.get('/api/users/:id', (req, res, ctx) => {
         return res(ctx.json({
           id: '1',
           name: 'John Doe',
           email: 'john@example.com'
         }));
       })
     );

     beforeAll(() => server.listen());
     afterEach(() => server.resetHandlers());
     afterAll(() => server.close());

     describe('UserProfile', () => {
       it('loads and displays user data', async () => {
         render(<UserProfile userId="1" />);

         // Initially shows loading state
         expect(screen.getByText(/loading/i)).toBeInTheDocument();

         // Wait for user data to load and display
         const userName = await screen.findByText('John Doe');
         const userEmail = await screen.findByText('john@example.com');

         expect(userName).toBeInTheDocument();
         expect(userEmail).toBeInTheDocument();

         // Loading indicator should be gone
         expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
       });

       it('handles loading errors gracefully', async () => {
         // Override the handler to return an error
         server.use(
           rest.get('/api/users/:id', (req, res, ctx) => {
             return res(ctx.status(500));
           })
         );

         render(<UserProfile userId="1" />);

         // Wait for error message to appear
         await waitFor(() => {
           expect(screen.getByText(/failed to load user/i)).toBeInTheDocument();
         });

         // Should have a retry button
         const retryButton = screen.getByRole('button', { name: /retry/i });
         expect(retryButton).toBeInTheDocument();
       });

       it('allows retrying failed requests', async () => {
         const user = userEvent.setup();

         // First request fails
         server.use(
           rest.get('/api/users/:id', (req, res, ctx) => {
             return res(ctx.status(500));
           })
         );

         render(<UserProfile userId="1" />);

         await waitFor(() => {
           expect(screen.getByText(/failed to load user/i)).toBeInTheDocument();
         });

         // Reset to successful response
         server.use(
           rest.get('/api/users/:id', (req, res, ctx) => {
             return res(ctx.json({
               id: '1',
               name: 'John Doe',
               email: 'john@example.com'
             }));
           })
         );

         // Click retry
         const retryButton = screen.getByRole('button', { name: /retry/i });
         await user.click(retryButton);

         // Should show user data after retry
         await waitFor(() => {
           expect(screen.getByText('John Doe')).toBeInTheDocument();
         });
       });
     });
     ```

---

## Component Testing Patterns

4. **Form Testing**
   - Test form validation, submission, and error handling
   - Use userEvent for realistic user interactions
   - Test accessibility of form elements
   - Example form testing:
     ```tsx
     import { render, screen, waitFor } from '@testing-library/react';
     import userEvent from '@testing-library/user-event';
     import ContactForm from './ContactForm';

     describe('ContactForm', () => {
       it('validates required fields', async () => {
         const user = userEvent.setup();
         const mockOnSubmit = vi.fn();

         render(<ContactForm onSubmit={mockOnSubmit} />);

         const submitButton = screen.getByRole('button', { name: /submit/i });
         await user.click(submitButton);

         // Should show validation errors
         expect(screen.getByText(/name is required/i)).toBeInTheDocument();
         expect(screen.getByText(/email is required/i)).toBeInTheDocument();
         expect(screen.getByText(/message is required/i)).toBeInTheDocument();

         // Should not have called onSubmit
         expect(mockOnSubmit).not.toHaveBeenCalled();
       });

       it('validates email format', async () => {
         const user = userEvent.setup();

         render(<ContactForm onSubmit={vi.fn()} />);

         const emailInput = screen.getByLabelText(/email/i);
         await user.type(emailInput, 'invalid-email');
         await user.tab(); // Trigger blur event

         expect(screen.getByText(/please enter a valid email/i)).toBeInTheDocument();
       });

       it('submits form with valid data', async () => {
         const user = userEvent.setup();
         const mockOnSubmit = vi.fn();

         render(<ContactForm onSubmit={mockOnSubmit} />);

         // Fill out form
         await user.type(screen.getByLabelText(/name/i), 'John Doe');
         await user.type(screen.getByLabelText(/email/i), 'john@example.com');
         await user.type(screen.getByLabelText(/message/i), 'Hello, this is a test message.');

         // Submit form
         await user.click(screen.getByRole('button', { name: /submit/i }));

         await waitFor(() => {
           expect(mockOnSubmit).toHaveBeenCalledWith({
             name: 'John Doe',
             email: 'john@example.com',
             message: 'Hello, this is a test message.'
           });
         });
       });

       it('disables submit button during submission', async () => {
         const user = userEvent.setup();
         const mockOnSubmit = vi.fn(() => new Promise(resolve => setTimeout(resolve, 1000)));

         render(<ContactForm onSubmit={mockOnSubmit} />);

         // Fill out form
         await user.type(screen.getByLabelText(/name/i), 'John Doe');
         await user.type(screen.getByLabelText(/email/i), 'john@example.com');
         await user.type(screen.getByLabelText(/message/i), 'Test message');

         const submitButton = screen.getByRole('button', { name: /submit/i });
         await user.click(submitButton);

         // Button should be disabled during submission
         expect(submitButton).toBeDisabled();
         expect(screen.getByText(/submitting/i)).toBeInTheDocument();
       });

       it('handles submission errors', async () => {
         const user = userEvent.setup();
         const mockOnSubmit = vi.fn().mockRejectedValue(new Error('Server error'));

         render(<ContactForm onSubmit={mockOnSubmit} />);

         // Fill out and submit form
         await user.type(screen.getByLabelText(/name/i), 'John Doe');
         await user.type(screen.getByLabelText(/email/i), 'john@example.com');
         await user.type(screen.getByLabelText(/message/i), 'Test message');
         await user.click(screen.getByRole('button', { name: /submit/i }));

         // Should show error message
         await waitFor(() => {
           expect(screen.getByText(/failed to send message/i)).toBeInTheDocument();
         });

         // Form should be re-enabled
         expect(screen.getByRole('button', { name: /submit/i })).toBeEnabled();
       });
     });
     ```

5. **Testing Custom Hooks**
   - Use renderHook for testing custom hooks in isolation
   - Test hook state changes and side effects
   - Mock dependencies and external APIs
   - Example hook testing:
     ```tsx
     import { renderHook, act, waitFor } from '@testing-library/react';
     import { rest } from 'msw';
     import { setupServer } from 'msw/node';
     import { useUserData } from './useUserData';

     const server = setupServer(
       rest.get('/api/users/:id', (req, res, ctx) => {
         return res(ctx.json({
           id: req.params.id,
           name: 'John Doe',
           email: 'john@example.com'
         }));
       })
     );

     beforeAll(() => server.listen());
     afterEach(() => server.resetHandlers());
     afterAll(() => server.close());

     describe('useUserData', () => {
       it('loads user data successfully', async () => {
         const { result } = renderHook(() => useUserData('1'));

         // Initially loading
         expect(result.current.loading).toBe(true);
         expect(result.current.user).toBeNull();
         expect(result.current.error).toBeNull();

         // Wait for data to load
         await waitFor(() => {
           expect(result.current.loading).toBe(false);
         });

         expect(result.current.user).toEqual({
           id: '1',
           name: 'John Doe',
           email: 'john@example.com'
         });
         expect(result.current.error).toBeNull();
       });

       it('handles loading errors', async () => {
         server.use(
           rest.get('/api/users/:id', (req, res, ctx) => {
             return res(ctx.status(500));
           })
         );

         const { result } = renderHook(() => useUserData('1'));

         await waitFor(() => {
           expect(result.current.loading).toBe(false);
         });

         expect(result.current.user).toBeNull();
         expect(result.current.error).toBe('Failed to load user');
       });

       it('refetches data when userId changes', async () => {
         const { result, rerender } = renderHook(
           ({ userId }) => useUserData(userId),
           { initialProps: { userId: '1' } }
         );

         // Wait for initial load
         await waitFor(() => {
           expect(result.current.user?.id).toBe('1');
         });

         // Setup different response for user 2
         server.use(
           rest.get('/api/users/2', (req, res, ctx) => {
             return res(ctx.json({
               id: '2',
               name: 'Jane Doe',
               email: 'jane@example.com'
             }));
           })
         );

         // Change userId
         rerender({ userId: '2' });

         await waitFor(() => {
           expect(result.current.user?.id).toBe('2');
         });

         expect(result.current.user?.name).toBe('Jane Doe');
       });

       it('allows manual refresh', async () => {
         const fetchSpy = vi.fn();
         server.use(
           rest.get('/api/users/:id', (req, res, ctx) => {
             fetchSpy();
             return res(ctx.json({
               id: req.params.id,
               name: 'John Doe',
               email: 'john@example.com'
             }));
           })
         );

         const { result } = renderHook(() => useUserData('1'));

         await waitFor(() => {
           expect(result.current.loading).toBe(false);
         });

         expect(fetchSpy).toHaveBeenCalledTimes(1);

         // Trigger refresh
         act(() => {
           result.current.refresh();
         });

         await waitFor(() => {
           expect(fetchSpy).toHaveBeenCalledTimes(2);
         });
       });
     });

     // Testing hooks with context
     import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

     describe('useUserData with React Query', () => {
       it('uses React Query context', async () => {
         const queryClient = new QueryClient({
           defaultOptions: {
             queries: { retry: false },
           },
         });

         const wrapper = ({ children }: { children: React.ReactNode }) => (
           <QueryClientProvider client={queryClient}>
             {children}
           </QueryClientProvider>
         );

         const { result } = renderHook(() => useUserData('1'), { wrapper });

         await waitFor(() => {
           expect(result.current.isLoading).toBe(false);
         });

         expect(result.current.data).toBeDefined();
       });
     });
     ```

---

## Integration Testing

6. **Testing Component Integration**
   - Test how components work together
   - Mock external dependencies and APIs
   - Test user workflows end-to-end
   - Example integration tests:
     ```tsx
     import { render, screen, waitFor } from '@testing-library/react';
     import userEvent from '@testing-library/user-event';
     import { BrowserRouter } from 'react-router-dom';
     import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
     import { rest } from 'msw';
     import { setupServer } from 'msw/node';
     import App from './App';

     const server = setupServer(
       rest.get('/api/products', (req, res, ctx) => {
         return res(ctx.json([
           { id: '1', name: 'Laptop', price: 999.99, category: 'electronics' },
           { id: '2', name: 'Book', price: 19.99, category: 'books' }
         ]));
       }),
       rest.post('/api/cart/items', (req, res, ctx) => {
         return res(ctx.json({ success: true }));
       })
     );

     beforeAll(() => server.listen());
     afterEach(() => server.resetHandlers());
     afterAll(() => server.close());

     const renderWithProviders = (ui: React.ReactElement) => {
       const queryClient = new QueryClient({
         defaultOptions: {
           queries: { retry: false },
           mutations: { retry: false },
         },
       });

       return render(
         <QueryClientProvider client={queryClient}>
           <BrowserRouter>
             {ui}
           </BrowserRouter>
         </QueryClientProvider>
       );
     };

     describe('Shopping workflow', () => {
       it('allows user to browse products and add to cart', async () => {
         const user = userEvent.setup();

         renderWithProviders(<App />);

         // Navigate to products page
         const productsLink = await screen.findByRole('link', { name: /products/i });
         await user.click(productsLink);

         // Wait for products to load
         await waitFor(() => {
           expect(screen.getByText('Laptop')).toBeInTheDocument();
           expect(screen.getByText('Book')).toBeInTheDocument();
         });

         // Add laptop to cart
         const laptopAddButton = screen.getByRole('button', {
           name: /add laptop to cart/i
         });
         await user.click(laptopAddButton);

         // Check for success feedback
         await waitFor(() => {
           expect(screen.getByText(/added to cart/i)).toBeInTheDocument();
         });

         // Verify cart counter updated
         const cartCounter = screen.getByText(/cart.*1/i);
         expect(cartCounter).toBeInTheDocument();
       });

       it('handles network errors gracefully', async () => {
         // Simulate network error
         server.use(
           rest.get('/api/products', (req, res, ctx) => {
             return res(ctx.status(500));
           })
         );

         renderWithProviders(<App />);

         const productsLink = await screen.findByRole('link', { name: /products/i });
         await user.click(productsLink);

         await waitFor(() => {
           expect(screen.getByText(/failed to load products/i)).toBeInTheDocument();
         });

         // Should have retry button
         const retryButton = screen.getByRole('button', { name: /retry/i });
         expect(retryButton).toBeInTheDocument();
       });

       it('filters products by category', async () => {
         const user = userEvent.setup();

         renderWithProviders(<App />);

         const productsLink = await screen.findByRole('link', { name: /products/i });
         await user.click(productsLink);

         // Wait for products to load
         await waitFor(() => {
           expect(screen.getByText('Laptop')).toBeInTheDocument();
           expect(screen.getByText('Book')).toBeInTheDocument();
         });

         // Filter by electronics category
         const electronicsFilter = screen.getByRole('button', { name: /electronics/i });
         await user.click(electronicsFilter);

         // Should only show laptop
         expect(screen.getByText('Laptop')).toBeInTheDocument();
         expect(screen.queryByText('Book')).not.toBeInTheDocument();

         // Clear filter
         const allFilter = screen.getByRole('button', { name: /all/i });
         await user.click(allFilter);

         // Should show all products again
         expect(screen.getByText('Laptop')).toBeInTheDocument();
         expect(screen.getByText('Book')).toBeInTheDocument();
       });
     });
     ```

7. **Mocking Strategies**
   - Use MSW for API mocking
   - Mock third-party libraries appropriately
   - Create reusable mock factories
   - Example mocking patterns:
     ```tsx
     // __mocks__/handlers.ts
     import { rest } from 'msw';

     export const handlers = [
       rest.get('/api/users', (req, res, ctx) => {
         const page = req.url.searchParams.get('page') || '1';
         const limit = req.url.searchParams.get('limit') || '10';

         const users = Array.from({ length: parseInt(limit) }, (_, i) => ({
           id: `${(parseInt(page) - 1) * parseInt(limit) + i + 1}`,
           name: `User ${i + 1}`,
           email: `user${i + 1}@example.com`
         }));

         return res(ctx.json({
           users,
           total: 100,
           page: parseInt(page),
           totalPages: Math.ceil(100 / parseInt(limit))
         }));
       }),

       rest.post('/api/users', (req, res, ctx) => {
         return res(ctx.json({
           id: '123',
           ...req.body,
           createdAt: new Date().toISOString()
         }));
       }),

       rest.get('/api/users/:id', (req, res, ctx) => {
         const { id } = req.params;
         return res(ctx.json({
           id,
           name: `User ${id}`,
           email: `user${id}@example.com`
         }));
       })
     ];

     // __mocks__/factories.ts
     export const createMockUser = (overrides = {}) => ({
       id: '1',
       name: 'John Doe',
       email: 'john@example.com',
       createdAt: '2023-01-01T00:00:00Z',
       ...overrides
     });

     export const createMockProduct = (overrides = {}) => ({
       id: '1',
       name: 'Test Product',
       price: 29.99,
       description: 'A test product',
       inStock: true,
       category: 'test',
       ...overrides
     });

     // Custom render with common providers
     // __tests__/test-utils.tsx
     import { render, RenderOptions } from '@testing-library/react';
     import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
     import { BrowserRouter } from 'react-router-dom';
     import { AuthProvider } from '../contexts/AuthContext';

     interface CustomRenderOptions extends RenderOptions {
       initialEntries?: string[];
       user?: any;
     }

     export const renderWithProviders = (
       ui: React.ReactElement,
       {
         initialEntries = ['/'],
         user = null,
         ...renderOptions
       }: CustomRenderOptions = {}
     ) => {
       const queryClient = new QueryClient({
         defaultOptions: {
           queries: { retry: false },
           mutations: { retry: false },
         },
       });

       const Wrapper = ({ children }: { children: React.ReactNode }) => (
         <QueryClientProvider client={queryClient}>
           <BrowserRouter>
             <AuthProvider initialUser={user}>
               {children}
             </AuthProvider>
           </BrowserRouter>
         </QueryClientProvider>
       );

       return {
         ...render(ui, { wrapper: Wrapper, ...renderOptions }),
         queryClient
       };
     };

     // Re-export everything
     export * from '@testing-library/react';
     export { renderWithProviders as render };
     ```

---

## Advanced Testing Patterns

8. **Testing Error Boundaries**
   - Test error boundary behavior
   - Verify error reporting and fallback UI
   - Test error recovery mechanisms
   - Example error boundary testing:
     ```tsx
     import { render, screen } from '@testing-library/react';
     import ErrorBoundary from './ErrorBoundary';

     const ThrowError = ({ shouldThrow }: { shouldThrow: boolean }) => {
       if (shouldThrow) {
         throw new Error('Test error');
       }
       return <div>No error</div>;
     };

     describe('ErrorBoundary', () => {
       // Suppress console.error for these tests
       const originalError = console.error;
       beforeAll(() => {
         console.error = vi.fn();
       });
       afterAll(() => {
         console.error = originalError;
       });

       it('renders children when there is no error', () => {
         render(
           <ErrorBoundary>
             <ThrowError shouldThrow={false} />
           </ErrorBoundary>
         );

         expect(screen.getByText('No error')).toBeInTheDocument();
       });

       it('renders error UI when there is an error', () => {
         const mockOnError = vi.fn();

         render(
           <ErrorBoundary onError={mockOnError}>
             <ThrowError shouldThrow={true} />
           </ErrorBoundary>
         );

         expect(screen.getByText(/something went wrong/i)).toBeInTheDocument();
         expect(mockOnError).toHaveBeenCalledWith(
           expect.any(Error),
           expect.objectContaining({
             componentStack: expect.any(String)
           })
         );
       });

       it('allows error recovery', async () => {
         const user = userEvent.setup();
         const { rerender } = render(
           <ErrorBoundary>
             <ThrowError shouldThrow={true} />
           </ErrorBoundary>
         );

         expect(screen.getByText(/something went wrong/i)).toBeInTheDocument();

         const retryButton = screen.getByRole('button', { name: /try again/i });
         await user.click(retryButton);

         // Re-render with no error
         rerender(
           <ErrorBoundary>
             <ThrowError shouldThrow={false} />
           </ErrorBoundary>
         );

         expect(screen.getByText('No error')).toBeInTheDocument();
       });
     });
     ```

9. **Performance Testing**
   - Test component rendering performance
   - Verify memoization and optimization
   - Test for unnecessary re-renders
   - Example performance testing:
     ```tsx
     import { render, act } from '@testing-library/react';
     import { vi } from 'vitest';
     import OptimizedList from './OptimizedList';

     describe('OptimizedList performance', () => {
       it('does not re-render child components unnecessarily', () => {
         const renderSpy = vi.fn();

         const ListItem = ({ item }: { item: any }) => {
           renderSpy();
           return <div>{item.name}</div>;
         };

         const items = [
           { id: 1, name: 'Item 1' },
           { id: 2, name: 'Item 2' },
           { id: 3, name: 'Item 3' }
         ];

         const { rerender } = render(
           <OptimizedList items={items} renderItem={ListItem} />
         );

         expect(renderSpy).toHaveBeenCalledTimes(3);

         // Add new item - only new item should render
         const newItems = [...items, { id: 4, name: 'Item 4' }];
         rerender(<OptimizedList items={newItems} renderItem={ListItem} />);

         expect(renderSpy).toHaveBeenCalledTimes(4); // Only one additional call
       });

       it('memoizes expensive calculations', () => {
         const expensiveCalculation = vi.fn(() => 'calculated value');

         const ExpensiveComponent = ({ data }: { data: any }) => {
           const result = useMemo(() => expensiveCalculation(data), [data]);
           return <div>{result}</div>;
         };

         const { rerender } = render(<ExpensiveComponent data="test" />);

         expect(expensiveCalculation).toHaveBeenCalledTimes(1);

         // Re-render with same data - should not recalculate
         rerender(<ExpensiveComponent data="test" />);

         expect(expensiveCalculation).toHaveBeenCalledTimes(1);

         // Re-render with different data - should recalculate
         rerender(<ExpensiveComponent data="new-test" />);

         expect(expensiveCalculation).toHaveBeenCalledTimes(2);
       });
     });
     ```

---

## Test Organization and Setup

10. **Test Configuration**
    - Set up proper test environment
    - Configure test utilities and helpers
    - Implement proper test cleanup
    - Example test setup:
      ```typescript
      // vitest.config.ts
      import { defineConfig } from 'vitest/config';
      import react from '@vitejs/plugin-react';

      export default defineConfig({
        plugins: [react()],
        test: {
          environment: 'jsdom',
          setupFiles: ['./src/test/setup.ts'],
          globals: true,
          css: true,
        },
      });

      // src/test/setup.ts
      import '@testing-library/jest-dom';
      import { cleanup } from '@testing-library/react';
      import { afterEach, beforeAll, afterAll } from 'vitest';
      import { server } from './mocks/server';

      // Start mock service worker
      beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
      afterEach(() => {
        server.resetHandlers();
        cleanup();
      });
      afterAll(() => server.close());

      // src/test/mocks/server.ts
      import { setupServer } from 'msw/node';
      import { handlers } from './handlers';

      export const server = setupServer(...handlers);

      // package.json scripts
      {
        "scripts": {
          "test": "vitest",
          "test:ui": "vitest --ui",
          "test:coverage": "vitest --coverage",
          "test:watch": "vitest --watch"
        }
      }
      ```

11. **Test Best Practices Checklist**
    - Organize tests logically and consistently
    - Use descriptive test names and structure
    - Keep tests focused and independent
    - Example test organization:
      ```typescript
      describe('UserProfileComponent', () => {
        // Setup and shared state
        const mockUser = createMockUser();

        beforeEach(() => {
          // Common setup for all tests
        });

        describe('rendering', () => {
          it('displays user information correctly', () => {
            // Test rendering behavior
          });

          it('shows loading state initially', () => {
            // Test loading state
          });

          it('handles missing user data gracefully', () => {
            // Test edge cases
          });
        });

        describe('user interactions', () => {
          it('allows editing profile when edit button is clicked', async () => {
            // Test user interactions
          });

          it('cancels edit mode when cancel is clicked', async () => {
            // Test interaction flows
          });
        });

        describe('error handling', () => {
          it('displays error message when update fails', async () => {
            // Test error scenarios
          });

          it('allows retrying failed operations', async () => {
            // Test error recovery
          });
        });
      });
      ```

---

## Summary Checklist

- [ ] Test user behavior, not implementation details
- [ ] Use semantic queries that mirror user interactions
- [ ] Write tests that promote accessible markup
- [ ] Handle async operations with waitFor and findBy queries
- [ ] Test form validation, submission, and error handling
- [ ] Use MSW for realistic API mocking
- [ ] Test custom hooks in isolation with renderHook
- [ ] Write integration tests for user workflows
- [ ] Test error boundaries and error recovery
- [ ] Verify performance optimizations work correctly
- [ ] Set up proper test environment and utilities
- [ ] Organize tests logically with clear descriptions
- [ ] Keep tests focused, independent, and maintainable
- [ ] Mock external dependencies appropriately
- [ ] Test loading states, error states, and edge cases

---

Follow these practices to write comprehensive, maintainable tests that give confidence in your React applications and promote better accessibility and user experience.
React Testing Library Best Practices - Cursor IDE AI Rule