←The AI Hub
SkillTestingStable2026

React Testing Architect

Writes and audits behavior-focused React tests with Testing Library, user-event, and MSW. Integration-first, deterministic async, and coverage that reflects real user outcomes.

Invoke
❯/react-testing-architect

Overview

React Testing Architect writes tests that reflect what users actually experience, not component internals. It leans integration-first, drives everything through real interactions, mocks the network at the boundary with MSW, and keeps async assertions deterministic so suites stay green under CI load.

What it does

01

Behavior over internals

Asserts on rendered output, roles, and user-visible state using semantic queries, so tests double as accessibility audits.

02

Deterministic async

Uses findBy and waitFor instead of arbitrary timeouts, killing the flakiness that passes locally and fails under CI load.

03

Coverage that matters

Targets risky branches, failure paths, and stuck-UI states over vanity numbers, with MSW scenarios for every API flow.

How it works

  1. 01

    Map the user story, dependencies, and required provider boundaries.

  2. 02

    Decide unit vs integration, defaulting to integration for UI.

  3. 03

    Enumerate scenarios: happy path, edge, loading, empty, and error.

  4. 04

    Write Arrange, Act, Assert tests with accessible queries and MSW.

  5. 05

    Audit for flakiness, redundancy, and coverage gaps, then report residual risk.

In practice

Exampletsx
it('shows validation message when email is invalid', async () => {
  renderWithProviders(<ProfileForm />)
  await userEvent.type(screen.getByLabelText(/email/i), 'nope')
  await userEvent.click(screen.getByRole('button', { name: /save/i }))
  expect(await screen.findByText(/enter a valid email/i)).toBeInTheDocument()
})

The skill

copy & drop into .claude/

react-testing-architect.md
namereact-testing-architect

descriptionWrite, audit, and improve unit and integration tests for React applications using Jest or Vitest, React Testing Library, user-event, and MSW. Use when creating tests for components/hooks/utilities, reviewing or refactoring existing tests, fixing flaky async tests, improving branch coverage, validating user flows, or adding stable test IDs when accessible selectors are insufficient.

React Testing Architect

Overview

Produce reliable, behavior-focused React tests that reflect real user outcomes. Prefer integration tests for components and user flows, and unit tests for pure logic and isolated state transitions. Optimize for confidence, readability, determinism, and maintainability.

Testing Philosophy

  • Test behavior users can observe, not component internals.
  • Assert on rendered output, accessibility roles, and user-visible state.
  • Drive tests via interactions with user-event.
  • Treat implementation details as private unless they are explicit contracts.
  • Keep tests deterministic, independent, and minimal in setup.
  • Default to integration-first for React components.

Test Distribution

  • 60 to 70 percent integration tests: component behavior, user flows, state wiring, API interactions.
  • 20 to 30 percent unit tests: pure functions, formatters, validators, reducers, selectors.
  • 5 to 10 percent complex edge-case tests: boundary conditions, permission logic, feature-flag combinations.

Over-indexing on unit tests leads to false confidence and brittle mocks.

Unit vs Integration

Choose unit when testing pure functions, reducers, selectors, or isolated hooks with renderHook where component context is unnecessary.

Choose integration when testing behavior from user interaction to rendered result: component plus hook plus state, component plus API (mock the network with MSW), async flows, loading/error/success states, and form submission.

When uncertain, start with one integration test for the critical flow, then add targeted unit tests for complex pure-logic branches.

Async Testing

Stable async tests are critical for CI. Flaky async behavior is the most common source of instability.

  • Use await screen.findBy* for elements that appear asynchronously.
  • Use waitFor for eventual state assertions, and waitForElementToBeRemoved for disappearance.
  • Never use setTimeout, arbitrary sleeps, or manual timing delays.
  • Assert the final UI state the user sees, not transient transitions.

Bad:

await new Promise(resolve =&gt; setTimeout(resolve, 500))
expect(screen.getByText(/data/i)).toBeInTheDocument()

Good:

expect(await screen.findByText(/data loaded/i)).toBeInTheDocument()

Failure-Mode Testing

Error paths are the highest-risk branches. Require tests for API failures (4xx, 5xx), network errors, permission errors (403), empty responses, unexpected exceptions, and validation failures. Verify user-visible outcomes: fallback UI, error messages, retry flows, graceful degradation, and error boundaries.

server.use(
  http.get('/api/users', () =&gt;
    HttpResponse.json({ message: 'Server error' }, { status: 500 })
  )
)

render(&lt;UserList /&gt;)
expect(await screen.findByText(/something went wrong/i)).toBeInTheDocument()
expect(screen.getByRole('button', { name: /retry/i })).toBeInTheDocument()

Query Priority

  1. getByRole, most accessible and semantic.
  2. getByLabelText, ideal for form inputs.
  3. getByPlaceholderText, when labels are absent.
  4. getByText, for static content.
  5. getByTestId, last resort when semantic queries are not viable.

Semantic queries reinforce accessibility, test what users perceive, resist implementation changes, and catch accessibility regressions automatically. Add data-testid only for non-semantic targets, named around product behavior (invoice-row-total), not styling.

Mocking Strategy

Mock at boundaries, not internals. Prefer real component composition and real hooks. Mock only the network (MSW), third-party analytics, unavailable browser APIs, and timers when testing timer behavior. Avoid mocking child components or internal hooks by default. Assert UI outcomes, not raw request details unless the contract itself is under test.

Coverage Strategy

Target meaningful coverage, not vanity numbers. Cover the happy path, error path, loading state, empty state, conditional-rendering branches, and permission/feature-flag branches. Prioritize risky untested branches over broad snapshots. Add high-value tests for abort flows, retry/resume, cleanup paths, and race-prone logic, and require tests for state transitions that can leave the UI stuck.

Test Structure

Use Arrange, Act, Assert. One behavioral intent per test. Name tests by behavior. Keep tests independent, and prefer factories over large inline objects.

it('shows validation message when email is invalid', async () =&gt; {
  // Arrange
  renderWithProviders(&lt;ProfileForm /&gt;)

  // Act
  await userEvent.type(screen.getByLabelText(/email/i), 'nope')
  await userEvent.click(screen.getByRole('button', { name: /save/i }))

  // Assert
  expect(await screen.findByText(/enter a valid email/i)).toBeInTheDocument()
})

Test Data Builders

Use factory functions instead of large inline objects to centralize defaults and clarify intent by specifying only relevant overrides.

function buildUser(overrides?: Partial&lt;User&gt;): User {
  return {
    id: 'user-1',
    name: 'Test User',
    email: 'test@example.com',
    role: 'editor',
    ...overrides,
  }
}

Accessibility Assertions

Accessibility contracts are part of user behavior. Assert enabled/disabled state, accessible names, alerts, and status text, and test keyboard navigation with userEvent.tab(). Semantic queries make tests accessibility audits by default.

Flaky-Test Prevention

Common causes: race conditions, arbitrary timing, time-based assertions, shared mutable state, order dependence, and non-deterministic data. Tests must be independent, clean up after themselves (unmount, reset mocks, clear stores), avoid timing assumptions, and use deterministic data.

beforeEach(() =&gt; {
  cleanup()
  vi.clearAllMocks()
  server.resetHandlers()
})

Auditing Existing Tests

Check that tests validate user-visible behavior, flag implementation-detail assertions, find missing branches and failure-mode tests, detect flaky patterns, remove redundant tests, improve queries toward accessible selectors, refactor shared setup, add MSW scenarios for API flows, and report residual risk.

State, Events, and Streams

Test full user-visible flows for state (interaction, action, transition, UI update) rather than store internals; unit-test reducers and selectors. Reset storage and test hydration for persisted stores. For event-driven and streaming code, test listener cleanup on unmount, unknown or malformed events, lifecycle transitions (preparing to streaming to completed, cancelled, or failed), and abort/resume flows.

Anti-Patterns

  • Testing private state or implementation internals.
  • Over-mocking components and hooks.
  • Snapshot overuse as the primary strategy.
  • Asserting on CSS classes or DOM shape when behavior is the target.
  • Time-based waits and arbitrary delays.
  • Coupled tests with shared mutable state.
  • Adding data-testid everywhere instead of fixing query strategy.

Workflow

  1. Understand the feature and the user story.
  2. Map dependencies: store, router, API, providers, flags.
  3. Decide unit vs integration.
  4. Enumerate scenarios: happy path, edge, loading, empty, error, conditional, and interaction variants.
  5. Configure MSW and mocking boundaries.
  6. Write deterministic Arrange, Act, Assert tests.
  7. Apply query priority; add data-testid only when required.
  8. Verify branch coverage and remove fragile tests.
  9. Audit readability, independence, and failure clarity.
  10. Improve rather than duplicate existing tests, and document gaps.

Built with

ClaudeTesting LibraryMSW
60-70%
integration-first
0
arbitrary waits
5
query tiers

Want one of these for your team?

I build custom AI workflows.

Let's talk→