# Organizing Tests

Course: [Playwright](https://www.devtutorials.ai/learn/playwright) · Section: Test Structure and Configuration
Source: https://www.devtutorials.ai/learn/playwright/test-structure/organizing-tests

> Grouping tests with test.describe, keeping tests independent, the four hooks and their order, test.step with boxed steps and return values, annotations (skip, fixme, fail, slow, conditional, only, custom), tags, picking tests with --grep, --only-changed and --last-failed, and generating tests from data.

## Organizing Tests

- One test is easy to read. Fifty tests in one file are not.
- Playwright gives you tools to group tests, share setup, name steps and pick which tests run.
- In this lesson, you will organize a growing test file for the Practice Shop.

**Simple lesson:** groups, hooks, steps and tags keep many tests easy to read and run.

### A Real-World Example

- Priya has three tests for the products page. Each one starts by opening the page.
- She puts them in a **group** called "products page" and moves the shared first step into a **hook**.

- The report shows each title as **products page › shows six products**.

### What You Will Learn

1. Groups with `test.describe()`, and why tests must stay independent.
2. The four hooks and the order they run in.
3. Steps with `test.step()`.
4. Annotations: skip, fixme, fail, slow and only.
5. Tags, and running only some tests.
6. Generating tests from data.

## Test Structure

### test.describe() and Nested Groups

- `test.describe('title', () => { ... })` puts tests in a named group.
- Groups can sit inside groups. The titles join up in reports: **cart › empty cart › shows a message**.
- Group by feature or page, not by test type.

### Keeping Tests Independent

- Every test gets a fresh page and a fresh browser context.
- A test must never need another test to run first. Tests may run in any order, at the same time, or alone.
- If two tests need the same setup, put it in a hook or a fixture, not in "test number one".

**Simple lesson:** group tests with `describe`, and make every test work on its own.

## Hooks

### The Four Hooks

| Hook | Runs | Use it for |
|---|---|---|
| `test.beforeEach()` | before **each** test in its group | opening the page, logging in |
| `test.afterEach()` | after **each** test, even a failed one | extra clean-up, logging |
| `test.beforeAll()` | **once**, before the first test of its group | expensive setup, such as test data |
| `test.afterAll()` | **once**, after the last test of its group | deleting that test data |

<figure className="plw-fig">
<svg viewBox="0 0 640 220" role="img" aria-labelledby="fig-plw-hooks" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-plw-hooks">Animation: the run order for a group with two tests: beforeAll, then beforeEach, test one, afterEach, then beforeEach, test two, afterEach, then afterAll</title>
  <g style={{ fontFamily: "var(--mono)", fontSize: 11, fontWeight: 700 }}>
    <rect x="10" y="90" width="72" height="40" rx="8" style={{ fill: "var(--plw-purple-soft)", stroke: "var(--plw-purple)" }} /><text x="46" y="114" textAnchor="middle" style={{ fill: "var(--plw-purple)" }}>beforeAll</text>
    <rect x="92" y="90" width="72" height="40" rx="8" style={{ fill: "var(--plw-blue-soft)", stroke: "var(--plw-blue)" }} /><text x="128" y="114" textAnchor="middle" style={{ fill: "var(--plw-blue)" }}>beforeEach</text>
    <rect x="174" y="90" width="60" height="40" rx="8" style={{ fill: "var(--plw-green-soft)", stroke: "var(--plw-green)" }} /><text x="204" y="114" textAnchor="middle" style={{ fill: "var(--plw-green)" }}>test 1</text>
    <rect x="244" y="90" width="66" height="40" rx="8" style={{ fill: "var(--plw-blue-soft)", stroke: "var(--plw-blue)" }} /><text x="277" y="114" textAnchor="middle" style={{ fill: "var(--plw-blue)" }}>afterEach</text>
    <rect x="320" y="90" width="72" height="40" rx="8" style={{ fill: "var(--plw-blue-soft)", stroke: "var(--plw-blue)" }} /><text x="356" y="114" textAnchor="middle" style={{ fill: "var(--plw-blue)" }}>beforeEach</text>
    <rect x="402" y="90" width="60" height="40" rx="8" style={{ fill: "var(--plw-green-soft)", stroke: "var(--plw-green)" }} /><text x="432" y="114" textAnchor="middle" style={{ fill: "var(--plw-green)" }}>test 2</text>
    <rect x="472" y="90" width="66" height="40" rx="8" style={{ fill: "var(--plw-blue-soft)", stroke: "var(--plw-blue)" }} /><text x="505" y="114" textAnchor="middle" style={{ fill: "var(--plw-blue)" }}>afterEach</text>
    <rect x="548" y="90" width="72" height="40" rx="8" style={{ fill: "var(--plw-purple-soft)", stroke: "var(--plw-purple)" }} /><text x="584" y="114" textAnchor="middle" style={{ fill: "var(--plw-purple)" }}>afterAll</text>
  </g>
  <rect y="84" width="10" height="52" rx="3" style={{ fill: "var(--plw-orange)" }}>
    <animate attributeName="x" values="36;118;194;267;346;422;495;574;574" keyTimes="0;0.12;0.24;0.36;0.48;0.6;0.72;0.84;1" dur="9s" repeatCount="indefinite" calcMode="discrete" />
  </rect>
  <text x="320" y="60" textAnchor="middle" style={{ fill: "var(--ink)", fontFamily: "var(--sans)", fontSize: 13 }}>test.describe('cart', ...) with two tests</text>
  <text x="320" y="170" textAnchor="middle" style={{ fill: "var(--muted)", fontFamily: "var(--sans)", fontSize: 12 }}>Each hooks run around every test. All hooks run once around the whole group.</text>
</svg>
<figcaption>The orange marker shows what runs, in order.</figcaption>
</figure>

- Run this file and open the **Terminal** tab. The `console.log` lines show the real order.

### Hook Scope in describe Blocks

- A hook belongs to the group it is written in. Outside any group, it applies to the whole file.
- Outer `beforeEach` hooks run before inner ones. Inner `afterEach` hooks run before outer ones.
- `beforeAll` gets worker fixtures such as `browser`, but not `page`. Each test gets its own page.

When a test fails, Playwright throws away its worker and starts a new one. Then `beforeAll` runs **again** for the next test. Do not count on `beforeAll` running exactly once per file.

**Simple lesson:** `beforeEach` and `afterEach` wrap each test. `beforeAll` and `afterAll` wrap the group.

## Test Steps

### test.step()

- `test.step('title', async () => { ... })` names a part of a test.
- Steps appear in the report, the trace and the lab's **Steps** tab. Long tests become easy to follow.

### Step Subtitles and Params

- `test.step('Add to cart', fn, { subtitle: 'Yoga Mat' })` adds a short subtitle next to the step title in reports and traces.
- `params: { product: 'Yoga Mat' }` attaches values that reporters can read. Both came in Playwright 1.63.

### Boxed Steps

- `test.step('log in', async () => { ... }, { box: true })` makes a **boxed** step.
- If something inside fails, the error points at the step, not at a line inside a helper.

### Returning Values from Steps

- A step can return a value: `const total = await test.step('read the total', async () => { ... })`.

**Simple lesson:** `test.step()` names the parts of a long test, and can return a value.

## Annotations

| Annotation | Effect |
|---|---|
| `test.skip(title, body)` | Declares a test that does not run |
| `test.skip(condition, 'why')` | Skips when the condition is true (inside a test or a group) |
| `test.fixme(title, body)` | Like skip, but means "broken, to be fixed" |
| `test.fail()` | Says the test **should** fail. Playwright reports a problem if it passes |
| `test.slow()` | Gives the test three times the normal timeout |
| `test.only(title, body)` | Runs only this test (and other `only` tests) |

### Conditional Annotations

- Skip tests that do not apply to a browser or an environment:

```ts
test('payment uses the Chrome wallet', async ({ page, browserName }) => {
  test.skip(browserName !== 'chromium', 'The wallet exists in Chromium only');
  // ...
});
```

### Custom Annotations

- Add your own notes with the `annotation` option. They show in the HTML report.
- Inside a test, `test.info().annotations.push({ type: 'issue', description: '...' })` adds one at run time.

- In WebKit, the second test is **skipped**. The fixme test never runs.
- The last test fails as expected, so it counts as a pass. Its heading really is "404: Page not found".

Never commit `test.only()`. It silently turns off every other test. Set `forbidOnly: !!process.env.CI` in the config, and CI refuses to run a file with `only` in it.

**Simple lesson:** annotations skip, mark or focus tests, and explain why.

## Tags

### Adding Tags with the tag Option

- A **tag** is a label that starts with `@`: `@smoke`, `@slow`, `@checkout`.
- Add tags with the `tag` option: `test('title', { tag: '@smoke' }, async ({ page }) => { ... })`.

### Tags in Test Titles and on describe Blocks

- A word that starts with `@` in a title is also a tag: `test('login works @smoke', ...)`.
- A tag on a `describe` block applies to every test inside it.

**Simple lesson:** tags label tests, so you can run a chosen set.

## Selecting Tests to Run

### --grep and --grep-invert

- `--grep @smoke` (or `-g`) runs tests whose title or tags match.
- `--grep-invert @slow` runs every test **except** those.
- `--grep` takes a regular expression: `--grep "@smoke|@checkout"` runs both.

### --only-changed and --last-failed

- `--last-failed` runs only the tests that failed in the last run. Great while you fix them.
- `--only-changed` runs tests in files that changed since the last git commit. It needs git, so it does not work in the lab.

- Try these commands: `--grep @checkout`, `--grep-invert @smoke`, `--list`, then `--grep "@smoke|@checkout"`.

**Simple lesson:** `--grep` and `--grep-invert` pick tests by title or tag. `--last-failed` re-runs failures.

## Parameterized Tests

### Generating Tests from Data

- Write the test once, and run it for every item in a list.
- A simple `for` loop around `test()` creates one test per item.

### Titles for Generated Tests

- Every generated test needs a **different** title. Put the data in the title.
- Two tests with the same title in one file cause an error.

Keep data close to the test. If the list grows large, move it to a separate file and import it.

**Simple lesson:** loop over data to create one test per case, each with its own title.

## Try It in Your Project

- These steps take the lesson from the lab to your own app.
- Group one spec file with `test.describe()` and move repeated setup into `beforeEach`.
- Tag your three most important tests `@smoke` and run them with `--grep @smoke`.
- Turn one copy-pasted set of tests into a data-driven loop.

**Simple lesson:** use one idea from this lesson on your own app this week.

## TL;DR

- `test.describe()` groups tests. Every test must work alone.
- `beforeEach`/`afterEach` wrap each test. `beforeAll`/`afterAll` wrap a group.
- `test.step()` names parts of a test, and can be boxed or return values.
- `skip`, `fixme`, `fail`, `slow` and `only` change how tests run. Never commit `only`.
- Tags plus `--grep` pick which tests run. `--last-failed` re-runs failures.
- A loop around `test()` makes parameterized tests. Give each a unique title.

## Common mistakes

| Mistake | What happens | Fix |
|---|---|---|
| Test 2 depends on data from test 1 | It fails when run alone or in another order | Set up data in a hook or inside the test |
| Using `page` in `beforeAll` | Playwright throws an error | Use `beforeEach`, or create a context from `browser` |
| Committing `test.only()` | All other tests stop running on CI | Use `forbidOnly: !!process.env.CI` |
| Same title for generated tests | Playwright reports duplicate titles | Put the data in each title |
| `test.skip()` with no reason | Nobody knows why it is skipped | Add a description string |

## Check Yourself

## Exercises

## What to Remember

- Group with `describe`. Keep each test independent.
- Hooks: each around tests, all around groups.
- Steps make long tests readable.
- Annotations and tags change what runs. Never commit `only`.
- Loops make data-driven tests with unique titles.

Next: **Fixtures**
