devto 2026-06-08 원문 보기 ↗
Welcome to Part 3 — API Testing. Until now the API was our setup helper. Now
we test it as a first-class surface. API tests need no browser, so they run in
milliseconds — and Inkwell speaks the documented RealWorld
API, so we're testing a real contract.
Code for this chapter is tagged
ch-11in the repo:
https://github.com/aktibaba/playwright-qa-course — see
src/tests/api/articles.spec.tsandsrc/setup/global-setup.ts.
Read assertions are only stable if the data is. In Part 1 individual tests reset the
database, which raced each other. The clean fix for a read-heavy API suite is to
seed once, before everything, and never reset mid-run:
// src/setup/global-setup.ts
import { request } from "@playwright/test";
import { env } from "../utils/env";
export default async function globalSetup(): Promise<void> {
const ctx = await request.newContext({ baseURL: `${env.apiURL}/` });
try {
const res = await ctx.post("test/reset");
if (!res.ok()) throw new Error(`reset failed: HTTP ${res.status()}`);
} finally {
await ctx.dispose();
}
}
// playwright.config.ts
export default defineConfig({
globalSetup: "./src/setup/global-setup.ts",
// ...
});
globalSetup runs once before any worker starts. Now every test reads a known
baseline, and because nothing resets during the run, read tests can't wipe each
other. (Tests that create data make their own and clean up — Chapter 13.)
api fixture is your client
We already have a worker-scoped api fixture — an APIRequestContext pointed at
the API. Its methods mirror HTTP: get, post, put, delete. Each returns an
APIResponse you assert on.
test("GET /articles lists the seeded article", async ({ api }) => {
const res = await api.get("articles");
expect(res.status()).toBe(200);
expect(res.headers()["content-type"]).toContain("application/json");
const body = await res.json();
expect(typeof body.articlesCount).toBe("number");
expect(Array.isArray(body.articles)).toBe(true);
const slugs = body.articles.map((a: { slug: string }) => a.slug);
expect(slugs).toContain("welcome-to-inkwell");
});
Three things to internalize:
res.status() vs res.ok(). ok() is true for any 2xx — fine for a happy
path. For anything where the exact code matters (especially errors), assert
status().res.json() is awaited and returns the parsed body. res.text() and
res.body() are there when you need raw payloads.res.headers() is a plain lowercase-keyed object — handy for asserting
content type, caching, or auth headers.Don't hand-build query strings — pass params and Playwright encodes them:
test("GET /articles respects the limit query param", async ({ api }) => {
const res = await api.get("articles", { params: { limit: 1 } });
expect(res.ok()).toBeTruthy();
const body = await res.json();
expect(body.articles.length).toBeLessThanOrEqual(1);
});
The RealWorld list endpoint also takes offset, tag, author, and favorited —
same mechanism for each.
A suite that only checks 200s misses half the contract. Inkwell returns a structured
404 for a missing article, and we assert both the status and the body shape:
test("GET /articles/:slug returns 404 for an unknown slug", async ({ api }) => {
const res = await api.get("articles/does-not-exist-xyz");
expect(res.status()).toBe(404);
const body = await res.json();
expect(body.errors.body[0]).toContain("not found");
});
Knowing the shape of an error ({ errors: { body: [...] } } here) is part of
testing an API contract — clients depend on it.
Notice what these tests don't do: no ${baseURL} plumbing (the api fixture owns
it), no manual context lifecycle (worker-scoped, Chapter 10), no data setup (global
seed). The fixture architecture from Part 2 pays off immediately — API specs are
almost pure assertions.
Reads are easy because they need no identity. Chapter 12 — Auth & sessions for the
API layer: log in once, get a token, and build an authedApi fixture (a chained
fixture, as promised in Chapter 9) so authenticated calls are as effortless as
anonymous ones. Tag: ch-12.
Following along? Star the repo
and tell me: do your API suites assert error responses, or only happy paths?