Testing the interface

Test an interface the way somebody uses it: find things by what they are, not by how they are styled.

10of 16
≈ 13Min read
5Parts

The query decides how brittle the test is

getByRole("button", { name: "Add to the quote" }) is a sentence about the product. document.querySelector(".btn-primary") is a sentence about the stylesheet — and it goes red the day somebody renames a class, while missing the day the button stops being a button.

This is the same list as the Accessibility course, from the other end: a component that is hard to query by role is usually one that is hard to use with a screen reader.

Find it bySurvivesUse
role and namea redesign, a class rename, a framework changefirst choice — it is what a user and a screen reader both see
label textanything but a wording changeform fields
texta wording changestatic content
test ideverythingthe escape hatch, when nothing above identifies it
CSS classnothingnever — a class is styling, not identity

Act like a user

Reaching into internal state tests the implementation, and it passes when the button that is supposed to trigger it has been disconnected. Dispatch the event the browser would dispatch, then assert on what appeared.

events, not method calls
// what a user does
fireEvent.click(getByRole("button", { name: "Add to the quote" }));
fireEvent.input(getByLabelText("Quantity"), { target: { value: "3" } });

// what a user cannot do
component.setState({ added: true });
component.instance().handleAdd();

Assert what is visible

the outcome, in the user’s words
fireEvent.click(getByRole("button", { name: "Add to the quote" }));

expect(getByRole("status")).toHaveTextContent("1 door in the quote");
expect(getByRole("button", { name: "Remove" })).toBeInTheDocument();
Not a snapshot of the markup

A full-markup snapshot goes red on every whitespace change and green on every logic bug. Its failure message is a diff of HTML nobody reads, and the fix is "update the snapshot" — which is how a snapshot suite becomes a rubber stamp. Assert the handful of things that matter.

Waiting, in a DOM

for the things that are not synchronous
fireEvent.click(getByRole("button", { name: "Load more" }));

// wrong: the fetch has not resolved yet
expect(getAllByRole("listitem")).toHaveLength(6);

// right: poll until it is true, or fail with a readable timeout
await waitFor(() => expect(getAllByRole("listitem")).toHaveLength(6));

// and the negative case has to wait too, or it passes before anything happened
await waitForElementToBeRemoved(() => queryByRole("progressbar"));

What a component test cannot tell you

It renders in a simulated DOM, not a browser: no real layout, no paint, no CSS in most setups, no scrolling, no actual focus ring. A component test proves the logic and the structure; it will never prove that the button is visible, reachable with a finger, or not covered by a banner. That is what the next lesson is for — and it is why a handful of real-browser tests earns its cost.

Practice