Claude
Skills
Sign in
Back

react-testing

Included with Lifetime
$97 forever

Testing patterns for React with Jest and React Testing Library. Use when writing tests, mocking modules, testing Zustand stores, or debugging test failures in React web applications.

Web Dev

What this skill does


# React Testing (Web)

## Problem Statement

React testing requires understanding component rendering, user interactions, and async state management. This skill covers Jest with React Testing Library patterns for web applications.

---

## Pattern: Zustand Store Testing

**Problem:** Store state persists between tests, causing flaky tests.

```typescript
import { useAppStore } from '@/stores/appStore';

const initialState = {
  items: [],
  loading: false,
  error: null,
};

describe('App Store', () => {
  // Reset store before each test
  beforeEach(() => {
    useAppStore.setState(initialState, true); // true = replace entire state
  });

  it('adds item to store', async () => {
    const store = useAppStore.getState();

    await store.addItem({ id: '1', name: 'Test' });

    expect(useAppStore.getState().items).toHaveLength(1);
  });

  it('handles loading state', async () => {
    const store = useAppStore.getState();

    const loadPromise = store.fetchItems();
    expect(useAppStore.getState().loading).toBe(true);

    await loadPromise;
    expect(useAppStore.getState().loading).toBe(false);
  });
});
```

**Key points:**
- Use `setState(initialState, true)` to replace (not merge) state
- Get fresh state with `getState()` after async operations
- Don't rely on component re-renders in store tests

---

## Pattern: Async Store Operations

**Problem:** Testing async Zustand actions with proper waiting.

```typescript
import { act, waitFor } from '@testing-library/react';

it('loads data correctly', async () => {
  const store = useAppStore.getState();

  // Wrap async store operations in act
  await act(async () => {
    await store.loadData('123');
  });

  // Verify state after async completes
  await waitFor(() => {
    const state = useAppStore.getState();
    expect(Object.keys(state.data).length).toBeGreaterThan(0);
  });
});

// For complex flows, verify each step
it('completes multi-step flow', async () => {
  const store = useAppStore.getState();

  // Step 1
  await act(async () => {
    await store.loadItems();
  });
  expect(useAppStore.getState().items).toBeDefined();

  // Step 2
  await act(async () => {
    await store.processItems();
  });
  expect(useAppStore.getState().processed).toBe(true);
});
```

---

## Pattern: Component Testing

```typescript
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

describe('ItemCard', () => {
  const mockItem = {
    id: '1',
    title: 'Test Item',
    price: 99.99,
  };

  it('displays item data', () => {
    render(<ItemCard item={mockItem} />);

    expect(screen.getByText('Test Item')).toBeInTheDocument();
    expect(screen.getByText('$99.99')).toBeInTheDocument();
  });

  it('calls onClick when clicked', async () => {
    const user = userEvent.setup();
    const onClick = jest.fn();

    render(<ItemCard item={mockItem} onClick={onClick} />);

    await user.click(screen.getByRole('button'));

    expect(onClick).toHaveBeenCalledWith(mockItem.id);
  });

  it('shows loading state', () => {
    render(<ItemCard item={mockItem} loading />);

    expect(screen.getByTestId('loading-spinner')).toBeInTheDocument();
  });
});
```

---

## Pattern: React Query Testing

**Problem:** Components using React Query need QueryClientProvider.

```typescript
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { render, screen, waitFor } from '@testing-library/react';

function createTestQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: {
        retry: false,
        gcTime: 0,
      },
    },
  });
}

function renderWithQuery(ui: React.ReactElement) {
  const queryClient = createTestQueryClient();
  return render(
    <QueryClientProvider client={queryClient}>
      {ui}
    </QueryClientProvider>
  );
}

// Usage in tests
it('fetches and displays data', async () => {
  renderWithQuery(<UserProfile userId="123" />);

  // Shows loading initially
  expect(screen.getByText('Loading...')).toBeInTheDocument();

  // Wait for data
  await waitFor(() => {
    expect(screen.getByText('John Doe')).toBeInTheDocument();
  });
});
```

---

## Pattern: Custom Hook Testing

```typescript
import { renderHook, act, waitFor } from '@testing-library/react';

describe('useAuth', () => {
  it('signs in user', async () => {
    const { result } = renderHook(() => useAuth(), {
      wrapper: AuthProvider, // If hook needs context
    });

    await act(async () => {
      await result.current.signIn('[email protected]', 'password');
    });

    expect(result.current.user).toBeDefined();
    expect(result.current.isAuthenticated).toBe(true);
  });

  it('handles sign in error', async () => {
    const { result } = renderHook(() => useAuth(), {
      wrapper: AuthProvider,
    });

    await act(async () => {
      try {
        await result.current.signIn('[email protected]', 'wrong');
      } catch (e) {
        // Expected
      }
    });

    expect(result.current.error).toBe('Invalid credentials');
  });
});

// Hook with Zustand
describe('useUserData', () => {
  beforeEach(() => {
    useUserStore.setState(initialState, true);
  });

  it('returns current user data', () => {
    // Pre-populate store
    useUserStore.setState({ user: { id: '1', name: 'Test' } });

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

    expect(result.current.user.name).toBe('Test');
  });
});
```

---

## Pattern: Mocking API Calls

```typescript
// Mock fetch globally
global.fetch = jest.fn();

beforeEach(() => {
  (fetch as jest.Mock).mockClear();
});

it('fetches user data', async () => {
  (fetch as jest.Mock).mockResolvedValueOnce({
    ok: true,
    json: async () => ({ id: '1', name: 'John' }),
  });

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

  await waitFor(() => {
    expect(screen.getByText('John')).toBeInTheDocument();
  });

  expect(fetch).toHaveBeenCalledWith('/api/users/1');
});

// Mock specific module
jest.mock('@/api/users', () => ({
  getUser: jest.fn(),
  updateUser: jest.fn(),
}));

import { getUser, updateUser } from '@/api/users';

it('loads and updates user', async () => {
  (getUser as jest.Mock).mockResolvedValue({ id: '1', name: 'John' });
  (updateUser as jest.Mock).mockResolvedValue({ id: '1', name: 'Jane' });

  // Test component that uses these
});
```

---

## Pattern: Router Testing

```typescript
import { MemoryRouter, Routes, Route } from 'react-router-dom';

function renderWithRouter(ui: React.ReactElement, { route = '/' } = {}) {
  return render(
    <MemoryRouter initialEntries={[route]}>
      {ui}
    </MemoryRouter>
  );
}

// Test navigation
it('navigates to profile on button click', async () => {
  const user = userEvent.setup();

  renderWithRouter(
    <Routes>
      <Route path="/" element={<HomePage />} />
      <Route path="/profile" element={<ProfilePage />} />
    </Routes>
  );

  await user.click(screen.getByText('Go to Profile'));

  expect(screen.getByText('Profile Page')).toBeInTheDocument();
});

// Test with route params
it('displays user from route params', async () => {
  renderWithRouter(
    <Routes>
      <Route path="/users/:id" element={<UserPage />} />
    </Routes>,
    { route: '/users/123' }
  );

  await waitFor(() => {
    expect(screen.getByText('User 123')).toBeInTheDocument();
  });
});
```

---

## Pattern: Form Testing

```typescript
import userEvent from '@testing-library/user-event';

describe('LoginForm', () => {
  it('submits form with entered data', async () => {
    const user = userEvent.setup();
    const onSubmit = jest.fn();

    render(<LoginForm onSubmit={onSubmit} />);

    await user.type(screen.getByLabelText('Email'), '[email protected]');
    await user.type(screen.getByLabelText('Password'), 'password123');
    await user.click(screen.getByRole('button', { name: 'Sign In' }));

    expect(onSubmit).toHaveBeenCalledWith({
      email: '[email protected]',
      password: 'password123',
    });
  });

Related in Web Dev