Organizing Tests
By Syncfusion®, Inc. · Last updated
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
- Groups with
test.describe(), and why tests must stay independent. - The four hooks and the order they run in.
- Steps with
test.step(). - Annotations: skip, fixme, fail, slow and only.
- Tags, and running only some tests.
- 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 |
Animation: the run order for a group with two tests: beforeAll, then beforeEach, test one, afterEach, then beforeEach, test two, afterEach, then afterAll
beforeAll beforeEach test 1 afterEach beforeEach test 2 afterEach afterAll
test.describe('cart', ...) with two tests Each hooks run around every test. All hooks run once around the whole group.
The orange marker shows what runs, in order.
- Run this file and open the Terminal tab. The
console.loglines 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
beforeEachhooks run before inner ones. InnerafterEachhooks run before outer ones. beforeAllgets worker fixtures such asbrowser, but notpage. 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:
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
annotationoption. 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
tagoption: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
describeblock 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 @slowruns every test except those.--greptakes a regular expression:--grep "@smoke|@checkout"runs both.
--only-changed and --last-failed
--last-failedruns only the tests that failed in the last run. Great while you fix them.--only-changedruns 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
forloop aroundtest()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 intobeforeEach. - Tag your three most important tests
@smokeand 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/afterEachwrap each test.beforeAll/afterAllwrap a group.test.step()names parts of a test, and can be boxed or return values.skip,fixme,fail,slowandonlychange how tests run. Never commitonly.- Tags plus
--greppick which tests run.--last-failedre-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