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
| Level | Runs in | Tells you | Breaks when |
|---|---|---|---|
| Unit — the function | milliseconds | Exactly which line is wrong | The function changes |
| API — the endpoint | tens of milliseconds | The service is wrong, roughly where | The contract changes |
| Browser — the whole flow | seconds | Something in a long chain is wrong | Anything 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();
});
Check yourself / اختبر نفسك
1. Why prefer the lowest level a check can be written at?
Speed matters, but the decisive property is the last one: a browser test breaks for layout, network and timing reasons that have nothing to do with the rule, so its failures are ambiguous in a way a unit test's never are.
2. What should a browser test exist to prove?
Rules belong lower, where a failure names the line. The thing only a browser test can show is that the whole chain connects — product page to completed order — which nothing below it can prove.
3. A team has 400 browser tests and 20 unit tests. What is the likely outcome?
It takes an hour, and a meaningful share of its failures will be timing and layout noise. People stop reading the results, and a suite nobody reads is worse than none — because the team still believes it is protected.
Score / النتيجة: 0 / 3