Test Naming & Structure
| Field | Value |
|---|---|
| Type | Skill Resource |
| Source | ~/.copilot/skills/quality/references/naming-structure.md |
| Description | Not specified |
Source Content
Test Naming & Structure
The convention exists so any engineer can read a failing test name and know what broke without opening the file. Consistency here is worth more than cleverness.
Table of contents
Naming
A test name describes observable behavior, not the method called. The pattern is does X when Y — present tense, no “should”, no implementation words.
| Avoid | Prefer |
|---|---|
it("test submit") | it("shows a confirmation when the email is valid") |
it("should call onSave") | it("saves the draft when the user clicks Save") |
it("renders correctly") | it("disables the button while the form is submitting") |
it("works") | it("rejects an amount above the account limit") |
Rules:
describethe unit,itthe behavior.describe("proratedAmount")groups; eachitis one behavior of it.- No “should”. It adds a word and no meaning.
it("returns zero when …"), notit("should return zero when …"). - No implementation words — no method names, no “calls”, no “state” in the behavior clause. Name what the user or caller observes.
- One behavior per
it. A name containing “and” is two tests.
Structure — Arrange-Act-Assert
Every test has three visually separated phases, in order, with a blank line between them:
it("rejects an amount above the account limit", () => { // Arrange const account = makeAccount({ limit: 5000 });
// Act const result = withdraw(account, 6000);
// Assert expect(result.ok).toBe(false); expect(result.error).toBe("LIMIT_EXCEEDED");});- Arrange — set up data, render, install handlers. Push repeated setup into a
beforeEachor a small factory (makeAccount), not copy-paste. - Act — the single action under test. If a test has two distinct actions, it is two tests.
- Assert — what changed. Assert the behavior, not the mechanism.
The comment markers are optional once the blank-line rhythm is habitual, but the three-phase order is not.
File and directory layout
- Co-locate the test with its subject:
billing.ts→billing.test.ts;SubscribeForm.tsx→SubscribeForm.test.tsx. - Use
.test.ts(x)(the house suffix) consistently; do not mix.specand.testin one repo. - Shared MSW handlers, render helpers, and factories live in a
test/orsrc/test-utils/folder and are imported — never redefined per file. - E2E specs live in their own top-level directory (e.g.
e2e/), separate from unit/component tests, because they run on a different command and schedule.
Worked example
A component test that exercises all the house layers at once — behavior, network, and a11y:
import { render, screen } from "@testing-library/react";import userEvent from "@testing-library/user-event";import { axe } from "jest-axe";import { http, HttpResponse } from "msw";import { server } from "../test/server";import { InvoiceList } from "./InvoiceList";
describe("InvoiceList", () => { it("renders each invoice returned by the API", async () => { // Arrange server.use( http.get("/api/invoices", () => HttpResponse.json([{ id: "inv_1", total: 14500 }]), ), );
// Act render(<InvoiceList />);
// Assert expect(await screen.findByText(/\$145\.00/)).toBeInTheDocument(); });
it("shows an error message when the request fails", async () => { // Arrange server.use( http.get("/api/invoices", () => new HttpResponse(null, { status: 500 })), );
// Act render(<InvoiceList />);
// Assert expect(await screen.findByText(/couldn’t load invoices/i)).toBeInTheDocument(); });
it("has no accessibility violations", async () => { // Arrange / Act const { container } = render(<InvoiceList />);
// Assert expect(await axe(container)).toHaveNoViolations(); });});Note: the amount is asserted as a locale-formatted string ($145.00), matching the house rule that all currency renders through formatCurrency (ADR-013). Tests assert the formatted output the user sees, not the raw 14500 integer.