Lesson 3 / الدرس 3

Where a test should live / أين ينبغي أن يعيش الاختبار

The same check can be written at three levels, and the level decides how fast it runs, how often it lies to you, and how clearly it says what broke. Choosing well is most of what separates a suite people trust from one they ignore.

الفحص نفسه يمكن أن يُكتب على ثلاثة مستويات، والمستوى يقرر: كم يجري سريعًا، وكم يكذب عليك، وكم يقول بوضوح ما الذي انكسر. وحسن الاختيار هو معظم ما يفصل مجموعة يثق بها الناس عن مجموعة يتجاهلونها.

Take one rule: a discount of 10% applies to orders over 500. You could check it by calling the function that calculates it, by calling the API that returns a priced order, or by driving a browser through a whole purchase. Same rule, three tests, wildly different costs — and the differences are not small.

One rule, three ways to check it

LevelRuns inTells youBreaks when
Unit — the functionmillisecondsExactly which line is wrongThe function changes
API — the endpointtens of millisecondsThe service is wrong, roughly whereThe contract changes
Browser — the whole flowsecondsSomething in a long chain is wrongAnything at all changes

Read the last column twice. A browser test breaks when the layout changes, when the network is slow, when a font loads late — none of which is the rule you were testing.

Push each check as low as it will go

That is the whole rule, and it is why the shape is usually drawn as a pyramid: many small fast tests at the bottom, fewer at the API, and a thin layer of browser tests at the top. A browser test should exist to prove the pieces are wired together — that you can actually get from the product page to a completed order — not to check that 10% of 500 is 50.

// The same rule, at the bottom of the pyramid. Milliseconds, and when it
// fails you know the line.
test('10% comes off orders over 500', () => {
  expect(priceOrder({ total: 501 }).discount).toBe(50.1);
  expect(priceOrder({ total: 500 }).discount).toBe(0);   // the boundary
});

// At the top. Seconds, and a failure could mean the rule, the layout,
// the network, or a slow font.
test('a customer can buy something', async ({ page }) => {
  await page.goto('/product/44');
  await page.getByRole('button', { name: 'Add to basket' }).click();
  await page.getByRole('link', { name: 'Checkout' }).click();
  await page.getByRole('button', { name: 'Place order' }).click();
  await expect(page.getByText(/ORD-/)).toBeVisible();
});
Both are worth having, and they are not alternatives. The first proves the rule, including its boundary. The second proves you can get from one end of the shop to the other. Writing the first as the second is the mistake this lesson exists to prevent.

Check yourself / اختبر نفسك

1. Why prefer the lowest level a check can be written at?

2. What should a browser test exist to prove?

3. A team has 400 browser tests and 20 unit tests. What is the likely outcome?