Skip to solution
mediumFrontend

How do you test React components?

880 views
01

Understand 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.

testingjestreact testing librarytdd
02

Attempt it yourself

Sketch your approach before reading the solution — that's what interviews test.

Nudge consolestandby

Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.

03

Study the solution

Target Audience: Engineers preparing for React interviews — assumes basic testing familiarity. Difficulty: Medium

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:. Section 3 is executed — the same three assertions run against a com

Solution ready — 2 min read

Classified // press E to declassify

04

Explore the playground snippets

The refactor test: one component, two versions, three assertions
Run Playground
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>
  );
}
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 44 of 119 decoded in the React.js track. One more won't hurt.

Back to track