react-testing

v2026.09.24

React component testing with Testing Library and Vitest. Covers unit tests, integration tests, async testing, mocking, user events, accessibility testing, and test patterns. USE WHEN: user mentions "React testing", "Testing Library", "Vitest", "component tests", "userEvent", "screen queries", "MSW", asks about "testing React components", "mocking in tests", "accessibility testing", "React test patterns" DO NOT USE FOR: E2E testing - use Playwright skill instead, backend testing - use framework-specific testing skills, general testing concepts - use testing framework skills

GitHub
安装命令
npx skhub add claude-dev-suite/react-testing
Markdown
SKILL.md

React Testing

Full Reference: See advanced.md for MSW setup, testing hooks, testing context, forms, accessibility testing, snapshots, and test patterns.

Deep Knowledge: Use mcp__documentation__fetch_docs with technology: react topic: testing for comprehensive documentation.

Setup with Vitest + Testing Library

// vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';

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

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

afterEach(() => {
  cleanup();
});

Basic Component Testing

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi } from 'vitest';

describe('Button', () => {
  it('renders children', () => {
    render(<Button onClick={() => {}}>Click me</Button>);
    expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument();
  });

  it('calls onClick when clicked', async () => {
    const handleClick = vi.fn();
    render(<Button onClick={handleClick}>Click me</Button>);

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

    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  it('is disabled when disabled prop is true', () => {
    render(<Button onClick={() => {}} disabled>Click me</Button>);
    expect(screen.getByRole('button')).toBeDisabled();
  });
});

Query Methods

// Priority order (prefer accessible queries)
// 1. getByRole - accessible to everyone
screen.getByRole('button', { name: /submit/i });
screen.getByRole('textbox', { name: /email/i });
screen.getByRole('heading', { level: 1 });

// 2. getByLabelText - for form fields
screen.getByLabelText(/email address/i);

// 3. getByPlaceholderText
screen.getByPlaceholderText(/enter your email/i);

// 4. getByText - for non-interactive elements
screen.getByText(/welcome to our app/i);

// 5. getByDisplayValue - for input values
screen.getByDisplayValue(/john@example.com/i);

// 6. getByAltText - for images
screen.getByAltText(/user avatar/i);

// 7. getByTitle
screen.getByTitle(/close/i);

// 8. getByTestId - last resort
screen.getByTestId('custom-element');

// Query variants
screen.getByRole('button');     // Throws if not found
screen.queryByRole('button');   // Returns null if not found
screen.findByRole('button');    // Returns Promise, waits for element
screen.getAllByRole('button');  // Returns array, throws if none
screen.queryAllByRole('button'); // Returns array (possibly empty)
screen.findAllByRole('button'); // Returns Promise of array

User Events

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

describe('Form', () => {
  it('submits with user input', async () => {
    const user = userEvent.setup();
    const handleSubmit = vi.fn();

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

    // Type in inputs
    await user.type(screen.getByLabelText(/email/i), 'john@example.com');
    await user.type(screen.getByLabelText(/password/i), 'secret123');

    // Click submit
    await user.click(screen.getByRole('button', { name: /sign in/i }));

    expect(handleSubmit).toHaveBeenCalledWith({
      email: 'john@example.com',
      password: 'secret123',
    });
  });

  it('handles keyboard navigation', async () => {
    const user = userEvent.setup();
    render(<Form />);

    // Tab through inputs
    await user.tab();
    expect(screen.getByLabelText(/email/i)).toHaveFocus();

    await user.tab();
    expect(screen.getByLabelText(/password/i)).toHaveFocus();

    // Type and submit with Enter
    await user.type(screen.getByLabelText(/password/i), 'secret{Enter}');
  });

  it('handles select and checkbox', async () => {
    const user = userEvent.setup();
    render(<SettingsForm />);

    // Select option
    await user.selectOptions(screen.getByRole('combobox'), ['dark']);

    // Toggle checkbox
    await user.click(screen.getByRole('checkbox', { name: /notifications/i }));

    expect(screen.getByRole('checkbox')).toBeChecked();
  });
});

Async Testing

describe('UserProfile', () => {
  it('shows loading then user data', async () => {
    render(<UserProfile userId="123" />);

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

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

    // Loading is gone
    expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
  });
});

// Using findBy (combines getBy + waitFor)
it('loads and displays items', async () => {
  render(<ItemList />);

  // findBy waits for element
  const items = await screen.findAllByRole('listitem');
  expect(items).toHaveLength(3);
});

// waitForElementToBeRemoved
it('removes loading indicator', async () => {
  render(<DataLoader />);

  await waitForElementToBeRemoved(() => screen.queryByText(/loading/i));

  expect(screen.getByText('Data loaded')).toBeInTheDocument();
});

Mocking

Mock Functions

import { vi } from 'vitest';

const mockFn = vi.fn();
mockFn.mockReturnValue('default');
mockFn.mockReturnValueOnce('first call');
mockFn.mockImplementation((x) => x * 2);
mockFn.mockResolvedValue({ data: [] });
mockFn.mockRejectedValue(new Error('Failed'));

// Assertions
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledTimes(2);
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2');
expect(mockFn).toHaveBeenLastCalledWith('last arg');

Mock Modules

// Mock a module
vi.mock('@/lib/api', () => ({
  fetchUsers: vi.fn(() => Promise.resolve([{ id: 1, name: 'John' }])),
}));

// Import after mocking
import { fetchUsers } from '@/lib/api';

it('fetches users', async () => {
  render(<UserList />);

  expect(fetchUsers).toHaveBeenCalled();
  await screen.findByText('John');
});

Common Pitfalls

IssueProblemSolution
Test not finding elementElement rendered asyncUse findBy or waitFor
State not updatingMissing act()Use userEvent (handles act)
Tests affecting each otherShared stateClean up in afterEach
Flaky testsRace conditionsUse proper async patterns

Best Practices

  • Test behavior, not implementation
  • Use accessible queries (getByRole)
  • Use userEvent over fireEvent
  • Test error states
  • Use MSW for API mocking
  • Don't test implementation details
  • Don't test third-party libraries
  • Don't overuse snapshots

When NOT to Use This Skill

  • End-to-end testing - Use Playwright skill for full E2E flows
  • Backend testing - Use framework-specific testing skills
  • Performance testing - Use specialized performance testing tools
  • Visual regression testing - Use tools like Percy or Chromatic

Anti-Patterns

Anti-PatternProblemSolution
Testing implementation detailsBrittle testsTest user-facing behavior
Using getByTestId firstNot testing accessibilityPrefer getByRole, getByLabelText
Using fireEvent instead of userEventDoesn't simulate real user interactionUse userEvent for realistic tests
Not waiting for async updatesFlaky testsUse findBy or waitFor
Snapshot testing everythingHard to maintainUse sparingly for stable UI
Testing third-party librariesWasted effortTrust library tests, test your integration
Not testing error statesMissing edge casesTest loading, error, empty states
Shallow renderingMissing integration issuesUse full render

Quick Troubleshooting

IssueLikely CauseFix
Element not foundQuery timing, wrong queryUse findBy for async or check query
Test timeoutAsync operation not completingCheck waitFor timeout, fix async code
Act warningState update outside actUse userEvent or wrap in act()
Flaky testsRace conditions, timingUse proper async queries (findBy, waitFor)
Can't test hooksTesting implementationExtract to component or use renderHook
Mock not workingMock after importMock before importing component
Tests affecting each otherShared stateClean up in afterEach

Reference Documentation

发现
标签

此技能尚未发布标签。

版本
最新版本元数据

版本

v2026.09.24

发布时间

Sep 24, 2026

分类

未分类

许可证

MIT

源路径

skills/frontend-frameworks/react-testing

默认分支

main

最新提交

9496306

Tree SHA

fe4e2f1