Day 11 of 20

API checks: test the data where it is born

Call the feed directly, then make the screen agree with it.

Lesson 12 of 210 complete

Your suite now drives the screen honestly; today it also checks what feeds the screen. Every price you have asserted so far came to you second-hand, through the page. The page gets it from the product feed, the same one you have been waiting on since Day 9. Playwright can call that feed directly, no browser involved, using the request fixture. A fixture is a ready-made tool Playwright hands your test; page, which you have used since Day 2, is one too. The feed answers in JSON, the plain text format APIs use to send structured data.

Two sources, one truth

The strongest data check compares the feed’s answer with the screen’s answer. If they disagree, someone is lying to the customer:

import { test, expect } from '@playwright/test';
import { TumbleKitchenPage } from '../pages/tumble-kitchen-page';

const FEED = 'https://juliapottinger.com/practice/tumble-kitchen/api/products.json';

test('verify that the plantain price on the card matches the feed', async ({ page, request }) => {
  const response = await request.get(FEED);
  expect(response.ok()).toBeTruthy();

  const data = await response.json();
  const plantain = data.products.find((p) => p.name === 'Deluxe Plantain Plate');
  const expected = plantain.price.toLocaleString('en-JM') + ' JMD';

  const store = new TumbleKitchenPage(page);
  await store.goto();
  await store.searchFor('patty');

  await expect(store.priceOf(store.firstCard())).toHaveText(expected);
});

Read it back: request.get fetches the raw feed, ok() confirms the server actually answered, and find, straight from Day 4, pulls the plantain plate out of the array. The expected label is built from the feed’s own number, then the assertion holds the card to it. Notice what this test does and does not pin: it does not care whether the plantain plate costs 4,500 or 4,800; it cares that the screen and the data agree. That is a different promise from Day 8’s exact-price test, and a suite wants both.

Pinning the value itself takes a second, smaller test, and it never opens a browser at all:

test('verify that the feed prices the Deluxe Plantain Plate at 4,500 JMD', async ({ request }) => {
  const response = await request.get(FEED);
  const data = await response.json();
  const plantain = data.products.find((p) => p.name === 'Deluxe Plantain Plate');

  expect(plantain.price).toBe(4500);
});

That test runs in a blink, because there is no page to render. When a suite is slow, this is usually the missed opportunity: checks that never needed a browser, paying for one anyway.

The contract idea, plainly

The feed is a promise to every screen that reads it: these fields, with these names, holding these types. The card code reaches for name and price; if the feed renames price to unitPrice next sprint, every screen breaks at once. A check against the feed catches that at the source, minutes earlier and far cheaper than a customer-facing failure. Teams call these contract tests. You have just written a small one.

When you start testing APIs in earnest, my API Testing Checklist is built for exactly that: the checks worth running on every endpoint, with live consoles to practise against.

The same idea against the live API

That feed is a static file. The store also runs a real REST API behind it, one that takes query parameters and answers in pages, at /api/tk/products. The same tool reaches it, the request fixture, no browser involved. What changes is that this endpoint makes a promise about shape, not just about prices: ask it for a page and it answers with the items on that page plus the counts that describe the whole set, page, pageSize, total, and totalPages. A test can hold it to that shape.

Here are two checks against it. The first pins the pagination contract; the second asks for a dish that does not exist and expects the store to say so:

import { test, expect } from '@playwright/test';

const API = 'https://juliapottinger.com/api/tk/products';

test('verify that the products endpoint answers with the pagination contract', async ({ request }) => {
  const response = await request.get(API + '?limit=12');
  expect(response.status()).toBe(200);

  const body = await response.json();
  expect(Array.isArray(body.items)).toBeTruthy();
  expect(body.page).toBe(1);
  expect(body.pageSize).toBe(12);
  expect(body.total).toBeGreaterThan(0);
  expect(body.totalPages).toBe(Math.ceil(body.total / body.pageSize));
});

test('verify that a missing product id returns 404', async ({ request }) => {
  const response = await request.get(API + '/no-such-dish');

  expect(response.status()).toBe(404);
});

Read them back. The first sends one real request, confirms the server answered 200, then holds the body to its promised shape: items is a list, page and pageSize are the numbers you asked for, total counts the whole catalogue, and totalPages is total divided by the page size, rounded up. That last line is the one worth having, because it fails the moment the counts stop agreeing with each other.

The second test is the interesting one, because it goes red, and the red is telling the truth. A request for a product id that does not exist should come back 404, the status that means not found. Run it and the status is 200, with an empty body where the dish should be. The endpoint is saying “here is your product” and handing over nothing, and a screen that trusts that answer renders a blank dish page instead of an honest not found. Do not touch the test. File the bug, keep the failing output as evidence, and leave the red where the whole team can see it, the same way you will with the cart total tomorrow. An API that returns 200 for a thing that is not there is lying politely, and a contract test is how you catch it.

Check the data where it is born, then check the screen agrees with it.

Do this today

Type both tests and run them. Then extend the first one: pick a second product, Full Jerk Combo, and assert its card matches its feed price the same way. You will find the pattern goes quickly, because find and the label-building are muscle memory by now. Then add the two API tests above, watch the contract test pass and the missing-id test go red, and write the bug report for the 404 that never came, with the failing output pasted in. Tomorrow the suite meets the cart, and catches something.

Before you move on

Do the practice, then mark it complete

Completion is for you. It means you produced the evidence the lesson asked for, not only that you reached the bottom of the page.