A test has setup, execution, evidence collection, and cleanup. Independent tests can run in any order.
It resembles preparing a clean desk for each science experiment.
prepare → test → record result → clean up
Series: Playwright Zero to Expert | Demo app used throughout this series: saucedemo.com
Every test you’ve written so far has stood alone. Real test suites have dozens, then hundreds, of tests, and they need shared setup, sensible grouping, and ways to selectively run (or skip) subsets of them. This part covers exactly that — the organizational scaffolding around individual tests, rather than the actions and assertions within them.
Hooks — beforeEach, afterEach, beforeAll, afterAll
Think about every test you’ve written that starts by logging into SauceDemo. Repeating those same three or four lines at the top of every single test file is exactly the kind of repetition Part 2’s discussion of loops warned against, applied here to test structure instead of data. Hooks let you run shared setup and teardown code automatically, around your actual tests:
import { test, expect } from "@playwright/test";
test.beforeEach(async ({ page }) => {
// Runs before EVERY test in this file
await page.goto("/");
await page.getByPlaceholder("Username").fill("standard_user");
await page.getByPlaceholder("Password").fill("secret_sauce");
await page.getByRole("button", { name: "Login" }).click();
});
test("user can add item to cart", async ({ page }) => {
// This test starts already logged in — beforeEach already ran
await page
.getByText("Sauce Labs Backpack")
.locator("..")
.getByRole("button", { name: "Add to cart" })
.click();
await expect(page.locator(".shopping_cart_badge")).toHaveText("1");
});
test("user can sort products by price", async ({ page }) => {
// This test ALSO starts already logged in — beforeEach ran again, fresh, for this test too
await page.getByLabel("Sort by").selectOption({ value: "lohi" });
// ...
});
Notice something important: beforeEach runs fresh, before every single test, not once for the whole file. This matters directly because of Part 6’s browser-context isolation — each test gets a brand new, clean context, so each test genuinely needs its own login; there’s no session left over from a previous test to rely on. afterEach runs after every test, regardless of whether it passed or failed — useful for cleanup that needs to happen no matter the outcome.
Analogy: The Hotel Room Housekeeping vs. The Lobby TV Imagine managing a hotel accommodating different guests (tests):
beforeEach/afterEach(Housekeeping): Before Guest 1 enters Room 101, the housekeeping crew sweeps the floors, makes the bed, and hangs clean towels (setup). When Guest 1 checks out, the crew sanitizes the room and removes any trash left behind (teardown). Guest 2 enters Room 101 knowing it is completely clean and isolated from whatever Guest 1 did.beforeAll/afterAll(Lobby TV): On Monday morning, you install a television in the hotel lobby (one-time setup). Every guest uses the lobby during their stay. However, if Guest 1 accidentally breaks the TV screen on Tuesday, the television remains broken for Guest 2 on Wednesday (leaked state pollution). You only dismantle the TV when the hotel shuts down at the end of the season (afterAll).
📊 Visual Flowchart: The Hooks Execution Pipeline
Here is the execution sequence of setup and teardown blocks running around isolated test blocks:
graph TD
beforeAll["beforeAll Hook (Runs once at start)"] --> beforeEach1["beforeEach Hook (Runs fresh for Test 1)"]
beforeEach1 --> Test1["Test 1 (Isolated browser context)"]
Test1 --> afterEach1["afterEach Hook (Teardown for Test 1)"]
afterEach1 --> beforeEach2["beforeEach Hook (Runs fresh for Test 2)"]
beforeEach2 --> Test2["Test 2 (Isolated browser context)"]
Test2 --> afterEach2["afterEach Hook (Teardown for Test 2)"]
afterEach2 --> afterAll["afterAll Hook (Runs once at the end)"]
beforeAll and afterAll run only once — before the very first test in a file, and after the very last one, respectively — appropriate for genuinely expensive, one-time setup that doesn’t need to be repeated per test (though, as you’ll see in Part 15, there’s an important, honest caveat about beforeAll and test isolation worth understanding before reaching for it by default).
describe — Grouping Related Tests
import { test, expect } from "@playwright/test";
test.describe("Cart functionality", () => {
test.beforeEach(async ({ page }) => {
// Setup specific to this group of tests
await page.goto("/");
// ... login ...
});
test("can add a single item", async ({ page }) => {
/* ... */
});
test("can add multiple items", async ({ page }) => {
/* ... */
});
test("can remove an item", async ({ page }) => {
/* ... */
});
});
test.describe("Checkout flow", () => {
test("requires all fields to be filled", async ({ page }) => {
/* ... */
});
test("calculates total correctly", async ({ page }) => {
/* ... */
});
});
describe groups related tests together, and — genuinely useful in practice — a beforeEach declared inside a describe block only applies to tests within that same block, not the entire file. This lets different logical groups of tests within one file have entirely different setup, without stepping on each other, and it makes a test report dramatically more readable, since results are shown grouped by their logical category rather than as one long, undifferentiated list.
skip, fixme, and only
test.skip('feature not yet implemented', async ({ page }) => { /* ... */ });
test.fixme('known broken, tracked in JIRA-1234', async ({ page }) => { /* ... */ });
test.only('the one test I'm actively debugging right now', async ({ page }) => { /* ... */ });
test.skip— deliberately doesn’t run this test at all, with the reason typically obvious from context or a comment. Genuinely useful for a test covering a feature that isn’t built yet, or one temporarily disabled for a known, understood reason.test.fixme— similar toskip, but semantically communicates “this is currently broken and needs fixing,” rather than “this is intentionally not applicable right now” — a small but real difference in meaning that helps whoever reads the test suite later understand why it’s not running.test.only— runs only this test, skipping every other test in the run entirely. Extremely useful while actively writing or debugging one specific test, since re-running an entire suite of hundreds of tests just to check one change is slow and unnecessary.
Here’s the one genuinely important warning about test.only, worth treating as a hard rule rather than a suggestion: it should never be committed to a shared repository. If test.only accidentally makes it into a pull request and gets merged, the entire CI pipeline will silently start running just that one test — while every other test in the suite quietly stops running at all, without any obvious error announcing it.
This is a genuinely real, if embarrassingly simple, way for a whole suite’s coverage to silently disappear for days or weeks before anyone notices. Many real teams configure a linting rule specifically to catch and block this from ever being committed — worth knowing this exists as a real, common safeguard, covered again properly in Part 31’s discussion of linting.
Annotations and Tags
Tests can be tagged for selective running — a common, genuinely practical need once a suite grows large:
test("user can complete checkout @smoke @regression", async ({ page }) => {
/* ... */
});
test("user can apply multiple discount codes @regression", async ({ page }) => {
/* ... */
});
npx playwright test --grep @smoke
This directly connects back to Part 0.5’s discussion of smoke versus regression testing — tagging lets the exact same physical test suite be run in different-sized slices depending on the situation: a fast @smoke subset on every single commit, and the full @regression set on a nightly schedule, for instance (Part 32 covers exactly this kind of CI scheduling properly).
How It Works in a Real Test Run
Each test should own its state. beforeEach prepares repeatable conditions, the test performs one meaningful scenario, and afterEach releases only resources that truly need explicit cleanup. beforeAll belongs to expensive worker-level setup, not shared mutable page state.
Tags and describe blocks organize selection and reporting; they do not create isolation. Leaving test.only in committed code can silently skip the rest of a CI suite, so production configuration should forbid it.
Interview Questions
Q: What’s the difference between beforeEach and beforeAll, and why does that difference matter for test isolation?
Ans: beforeEach runs fresh before every individual test, while beforeAll runs only once, before the very first test in the group. This matters for isolation because each test in Playwright gets its own clean browser context by default, meaning session state like a login doesn’t carry over between tests — beforeEach re-establishes that needed state for every test independently, while beforeAll would only set it up once, potentially leaving later tests without the setup they actually need if that setup was something context-specific like a login.
Q: Why does declaring beforeEach inside a describe block behave differently than declaring it at the top level of a file?
Ans: A beforeEach inside a describe block only applies to the tests within that specific block, not the whole file, which lets different logical groups of tests within the same file have entirely different, independent setup without interfering with each other’s requirements.
Q: What’s the practical difference in meaning between test.skip and test.fixme, even though both prevent a test from running?
Ans: test.skip generally communicates that a test is intentionally not applicable right now — perhaps testing a feature that doesn’t exist yet. test.fixme communicates that the test represents a genuinely known, currently broken issue that needs to be fixed. The distinction matters for anyone reading the suite later, since it signals a different kind of follow-up action — “nothing to do yet” versus “this needs attention.”
Q: Why is test.only considered dangerous if accidentally committed to a shared repository?
Ans: It restricts an entire test run to just that one test, silently skipping every other test in the suite without raising any obvious error. If merged into a shared branch, this can cause an entire CI pipeline to quietly stop running the vast majority of the suite’s tests for an extended period, with the loss of coverage going unnoticed unless someone specifically catches it — which is exactly why many real teams add automated linting rules to actively block it from being committed at all.
Q: How would you structure a test suite so that a fast subset can run on every commit while the full suite only runs nightly?
Ans: I’d tag tests meaningfully — for example, marking a small, critical set of tests as @smoke and the broader set as @regression — and configure CI to run only the @smoke-tagged tests via --grep @smoke on every commit for fast feedback, while scheduling a full run of the entire suite, including @regression, on a nightly or less frequent schedule where longer execution time is acceptable.
Exercises — Part 14
Understand:
Explain, in your own words, why relying on beforeAll to log in once and share that login across multiple tests would be risky, given what you know about browser contexts from Part 6.
Simple Practice:
Take three or four tests you’ve written earlier in this series that all start with the same SauceDemo login steps, and refactor them into a single file using test.describe and a shared beforeEach to eliminate the repetition.
Real-World Scenario:
Your team’s full regression suite takes 45 minutes to run, which is too slow to run on every single commit, but you still want fast feedback on the most critical flows. Design a tagging strategy (which tests would you tag @smoke, and why specifically those) and explain how you’d configure CI to use it — you don’t need real CI syntax yet, just the reasoning.
Challenge:
Set up a small ESLint rule (research this, since it’s a genuine, common real-world practice) that would flag or error on any committed test.only in a project. Write down what you found, even if you don’t fully implement it yet.
Next: Part 15 — Fixtures
— why fixtures exist, the difference between test-scoped and worker-scoped fixtures, and building your own custom fixtures for real, reusable test setup.
- Author
- TechByteByByte Editorial Team
- Reviewed by
- TechByteByByte Admin
- Published
- Last reviewed