Skip to the solution
MediumFrontend7 min read

How do you test React components?

880 views

The problem

Question presented to candidate: "How do you decide what to test in a React component, and what do you avoid testing?"

What a strong answer should cover:

  • Test behaviour, not implementation. Render the component, interact with it the way a user would, and assert on what is visible β€” not on state, not on which hooks ran.
  • The concrete test of whether you got that right: a pure refactor must not break the test. If restyling the markup breaks it, the test was coupled to implementation.
  • Query priority matters: prefer accessible queries β€” by role with an accessible name, by label, by text β€” over test ids, and never CSS classes. Role queries double as an accessibility check.
  • getBy throws if not found, queryBy returns null (use it to assert absence), findBy is async (use it for anything that appears later).
  • Use userEvent over raw fireEvent where available β€” it simulates the full interaction sequence rather than dispatching one synthetic event.
  • The pyramid in practice: many small unit/component tests, some integration tests across a few components, few end-to-end tests for critical journeys.
  • Do not test the library β€” that a useState updates is React's problem. Test the behaviour it produces.
  • Mock at the network boundary rather than mocking your own modules, so the test exercises real component wiring.

Clarifying questions expected:

  • "Is this a shared component or a page-level flow?" β€” that changes the level to test at.
  • "Do we have accessible names on these controls?" β€” if not, the test problem is really an accessibility problem.

Code / implementation expected: Optional. One good test is more convincing than a description.

  • testing
  • jest
  • react testing library
  • tdd

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

7 min read Β· 9 sections

Try it in the playground

Open any example in the playground to run and change it.

ReactThe refactor test: one component, two versions, three assertions
import { useState } from "react";

// The playground cannot run a test runner, so this reproduces the experiment
// live: the same three query strategies, applied to two versions of the same
// component. V2 is a PURE REFACTOR β€” identical behaviour, restyled markup.

function CounterV1() {
  const [n, setN] = useState(0);
  return (
    <div className="counter-box">
      <p data-testid="count-display" className="count">Count: {n}</p>
      <button className="btn-primary" onClick={() => setN((v) => v + 1)}>Increment</button>
    </div>
  );
}

function CounterV2() {
  const [n, setN] = useState(0);
  return (
    <section className="Counter_root__x7f2a">
      <span className="Counter_value__9bd1c" role="status">Count: {n}</span>
      <button className="Counter_button__2ke9x" onClick={() => setN((v) => v + 1)}>Increment</button>
    </section>
  );
}

// The three query strategies, run against whatever is in the container.
const STRATEGIES = [
  {
    name: "by role + accessible name",
    good: true,
    run: (root) => {
      const btn = [...root.querySelectorAll("button")]
        .find((b) => /increment/i.test(b.textContent || b.getAttribute("aria-label") || ""));
      if (!btn) throw new Error("no button with the accessible name Increment");
      return "found: " + JSON.stringify(btn.textContent);
    },
  },
  {
    name: "by test id",
    good: false,
    run: (root) => {
      const el = root.querySelector('[data-testid="count-display"]');
      if (!el) throw new Error('Unable to find an element by: [data-testid="count-display"]');
      return "found: " + JSON.stringify(el.textContent);
    },
  },
  {
    name: "by CSS class",
    good: false,
    run: (root) => {
      const el = root.querySelector(".count");
      if (!el) throw new Error("Unable to find an element with class .count");
      return "found: " + JSON.stringify(el.textContent);
    },
  },
];

export default function App() {
  const [v2, setV2] = useState(false);
  const [results, setResults] = useState(null);

  const check = () => {
    const root = document.getElementById("subject");
    setResults(STRATEGIES.map((s) => {
      try { return { name: s.name, good: s.good, ok: true, detail: s.run(root) }; }
      catch (e) { return { name: s.name, good: s.good, ok: false, detail: e.message }; }
    }));
  };

  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.6, maxWidth: 560 }}>
      <div style={{ display: "flex", gap: 8, marginBottom: 10 }}>
        <button onClick={() => { setV2(false); setResults(null); }} style={{ fontWeight: !v2 ? "bold" : "normal" }}>
          V1 (original)
        </button>
        <button onClick={() => { setV2(true); setResults(null); }} style={{ fontWeight: v2 ? "bold" : "normal" }}>
          V2 (pure refactor)
        </button>
        <button onClick={check}>run the three assertions</button>
      </div>

      <div id="subject" style={{ border: "1px solid #ddd", borderRadius: 8, padding: 12, marginBottom: 10 }}>
        {v2 ? <CounterV2 /> : <CounterV1 />}
      </div>

      {results && results.map((r) => (
        <div key={r.name} style={{
          border: "1px solid " + (r.ok ? "#cde3cd" : "#e0b4b4"),
          background: r.ok ? "#f2f9f2" : "#fdf0f0",
          borderRadius: 6, padding: 8, marginBottom: 6, fontSize: 13,
        }}>
          <strong>{r.ok ? "PASS" : "FAIL"}</strong> Β· {r.name}
          <div style={{ fontSize: 12, color: "#555" }}>{r.detail}</div>
        </div>
      ))}

      <p style={{ fontSize: 13, color: "#666" }}>
        Run the assertions against V1: all three pass, so all three look like
        good tests. Switch to V2 β€” same behaviour, restyled markup β€” and run
        them again. Only the one that asked for what the <em>user</em> sees
        survives. That is the diagnostic for whether a test is coupled to
        implementation, and you can only apply it by actually refactoring.
      </p>
    </div>
  );
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 44 of 119 in React.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All React.js questions
Up nextMedium

RSC vs Client Components β€” when do you add 'use client' in Next.js 15?

Continue