react-testing

star 2

React testing with Vitest, React Testing Library, and MSW. Focuses on user-centric testing, component isolation, and API mocking. Use when writing tests for React components, hooks, or debugging test failures in frontend code.

1ambda By 1ambda schedule Updated 12/30/2025

name: react-testing description: React testing with Vitest, React Testing Library, and MSW. Focuses on user-centric testing, component isolation, and API mocking. Use when writing tests for React components, hooks, or debugging test failures in frontend code.

React Testing

Testing patterns for React 19 + TypeScript projects with Vitest.

When to Use

  • Writing component tests
  • Testing custom hooks
  • Mocking API calls with MSW
  • Debugging test failures
  • Improving frontend test coverage

MCP Workflow

# 1. Find existing test patterns
serena.search_for_pattern("describe|it|test", relative_path="src/", paths_include_glob="**/*.test.{ts,tsx}")

# 2. Check test utilities
serena.get_symbols_overview(relative_path="src/test/")

# 3. Find component test patterns
jetbrains.search_in_files_by_text("render(", fileMask="*.test.tsx")

# 4. React Testing Library docs
context7.get-library-docs("/testing-library/react-testing-library", "queries")

Testing Principles

  1. Test user behavior, not implementation
  2. Query by accessibility: getByRole > getByTestId
  3. Avoid testing internal state directly
  4. Mock at boundaries: API calls, not internal functions

Query Priority

Priority Query Use When
1 getByRole Interactive elements (button, input)
2 getByLabelText Form fields
3 getByPlaceholderText Input with placeholder
4 getByText Non-interactive text
5 getByTestId Last resort

Component Test Patterns

Basic Component Test

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { UserCard } from './UserCard';

describe('UserCard', () => {
  it('should display user name', () => {
    render(<UserCard user={{ id: '1', name: 'Alice' }} />);

    expect(screen.getByRole('heading')).toHaveTextContent('Alice');
  });

  it('should call onSelect when clicked', async () => {
    const user = userEvent.setup();
    const handleSelect = vi.fn();

    render(
      <UserCard
        user={{ id: '1', name: 'Alice' }}
        onSelect={handleSelect}
      />
    );

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

    expect(handleSelect).toHaveBeenCalledWith('1');
  });
});

Async Component Test

import { render, screen, waitFor } from '@testing-library/react';
import { QueryClientProvider } from '@tanstack/react-query';
import { UserList } from './UserList';

describe('UserList', () => {
  it('should show loading then data', async () => {
    render(
      <QueryClientProvider client={queryClient}>
        <UserList />
      </QueryClientProvider>
    );

    // Loading state
    expect(screen.getByRole('progressbar')).toBeInTheDocument();

    // Wait for data
    await waitFor(() => {
      expect(screen.getByRole('list')).toBeInTheDocument();
    });

    expect(screen.getAllByRole('listitem')).toHaveLength(3);
  });
});

Custom Hook Testing

import { renderHook, waitFor } from '@testing-library/react';
import { useDebounce } from './useDebounce';

describe('useDebounce', () => {
  beforeEach(() => {
    vi.useFakeTimers();
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it('should debounce value changes', async () => {
    const { result, rerender } = renderHook(
      ({ value }) => useDebounce(value, 500),
      { initialProps: { value: 'initial' } }
    );

    expect(result.current).toBe('initial');

    rerender({ value: 'updated' });
    expect(result.current).toBe('initial'); // Not updated yet

    vi.advanceTimersByTime(500);
    await waitFor(() => {
      expect(result.current).toBe('updated');
    });
  });
});

MSW API Mocking

Setup

// src/test/mocks/handlers.ts
import { http, HttpResponse } from 'msw';

export const handlers = [
  http.get('/api/users', () => {
    return HttpResponse.json([
      { id: '1', name: 'Alice' },
      { id: '2', name: 'Bob' },
    ]);
  }),

  http.post('/api/users', async ({ request }) => {
    const body = await request.json();
    return HttpResponse.json({ id: '3', ...body }, { status: 201 });
  }),

  http.get('/api/users/:id', ({ params }) => {
    return HttpResponse.json({ id: params.id, name: 'User' });
  }),
];

Test Setup

// src/test/setup.ts
import { beforeAll, afterEach, afterAll } from 'vitest';
import { setupServer } from 'msw/node';
import { handlers } from './mocks/handlers';

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

beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

Override Handler in Test

import { server } from '@/test/setup';
import { http, HttpResponse } from 'msw';

it('should handle API error', async () => {
  server.use(
    http.get('/api/users', () => {
      return HttpResponse.json({ error: 'Server error' }, { status: 500 });
    })
  );

  render(<UserList />);

  await waitFor(() => {
    expect(screen.getByText(/error/i)).toBeInTheDocument();
  });
});

Testing Patterns for TanStack Query

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

function createTestQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: {
        retry: false,        // Don't retry in tests
        gcTime: Infinity,    // Keep cache during test
      },
    },
  });
}

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

// Usage
render(<UserList />, { wrapper });

Anti-Patterns

Pattern Problem Solution
getByTestId for buttons Tests implementation Use getByRole('button')
Testing state directly Brittle Test rendered output
fireEvent for clicks Doesn't match user Use userEvent
Mocking child components Over-isolation Render real children
await waitFor(() => {}) empty Flaky Wait for specific element
Testing third-party libs Wasted effort Trust libraries work

TDD Workflow for React

1. RED: Write Failing Test

it('should show error when name is empty', async () => {
  const user = userEvent.setup();
  render(<CreateUserForm />);

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

  expect(screen.getByRole('alert')).toHaveTextContent('Name is required');
});

2. GREEN: Implement

export function CreateUserForm() {
  const [error, setError] = useState<string | null>(null);

  const handleSubmit = (e: FormEvent) => {
    e.preventDefault();
    const form = e.target as HTMLFormElement;
    if (!form.name.value) {
      setError('Name is required');
      return;
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" />
      {error && <div role="alert">{error}</div>}
      <button type="submit">Submit</button>
    </form>
  );
}

3. REFACTOR: Improve accessibility, extract validation

Quality Checklist

  • Tests query by role/label, not testId
  • userEvent used instead of fireEvent
  • Async operations use waitFor
  • MSW for API mocking (not vi.mock)
  • Loading/error states tested
  • No testing of implementation details
  • Test names describe user behavior
Install via CLI
npx skills add https://github.com/1ambda/dataops-platform --skill react-testing
Repository Details
star Stars 2
call_split Forks 1
navigation Branch main
article Path SKILL.md
More from Creator