Day 10 of 20

Test data and isolation: every test builds its own world

Six password lengths, one loop, and tests that pass in any order.

Lesson 11 of 210 complete

Your waits are honest; today your tests stop leaning on each other. The pattern I keep meeting in inherited suites: test one signs in, test two assumes it is signed in, test three assumes the cart test two built. Run them in order and they pass. Run one alone, or in parallel, or after a failure, and they collapse like dominoes, and the failure always points at the wrong suspect.

The rule that prevents all of it: every test builds its own world, from nothing, every time.

beforeEach: the fresh start

Playwright gives you a hook that runs before every test in a file. Put the world-building there:

import { test, expect } from '@playwright/test';
import { TumbleKitchenPage } from '../pages/tumble-kitchen-page';

let store;

test.beforeEach(async ({ page }) => {
  store = new TumbleKitchenPage(page);
  await store.goto();
});

Read it back: before every single test, a fresh page object on a fresh tab, navigated clean. On Tumble Kitchen the cart lives in the page, so a fresh goto is an empty cart by construction. On a real product you would go further, creating the data each test needs through the API and cleaning up afterwards, but the principle is identical: no test inherits another test’s leftovers.

The payoff: one table, six tests

Now the JavaScript from Days 3 and 4 goes to work on something real. The sign-in form promises a password of 8 to 64 characters. That is two boundaries, and boundaries deserve values on both sides of each edge: 7, 8, 9, 63, 64, 65. Writing six near-identical tests by hand invites six chances to typo. Write the table once and loop instead.

First, teach the page object to sign in. Add the locators to the constructor and the action below it:

this.signinError = page.getByTestId('signin-error');

async signIn(email, password) {
  await this.page.getByTestId('signin-email').fill(email);
  await this.page.getByTestId('signin-password').fill(password);
  await this.page.getByTestId('signin-submit').click();
}

Then the loop, in a new file, tests/signin-boundaries.spec.js, under the beforeEach above:

const cases = [
  { length: 7, outcome: 'rejected' },
  { length: 8, outcome: 'accepted' },
  { length: 9, outcome: 'accepted' },
  { length: 63, outcome: 'accepted' },
  { length: 64, outcome: 'accepted' },
  { length: 65, outcome: 'rejected' },
];

for (const c of cases) {
  test('verify that a ' + c.length + ' character password is ' + c.outcome, async () => {
    await store.signIn('demo@tumblekitchen.test', 'a'.repeat(c.length));

    if (c.outcome === 'rejected') {
      await expect(store.signinError).toHaveText('Password must be 8 to 64 characters.');
    } else {
      await expect(store.signinError).toHaveText('Email or password is wrong. The demo sign-in is shown above.');
    }
  });
}

Read it back: an array of objects, a for..of loop, and string joining in the test name, all from Day 4, driving the real form six times. The rejected edges must see the exact length message. The accepted edges get past the length rule and fail on credentials instead, and that credentials message is the proof the rule let them through; the form told you which check it reached. Run it and you get six separate named tests in the report, each one green or red on its own.

Every test builds its own world, and no test inherits another test’s leftovers.

Do this today

Build the file and run it. Then prove the isolation: run the file alone, run the whole suite, run npx playwright test —workers 4 so tests execute in parallel, and confirm every answer stays the same. If any test changes its answer based on company, it is leaning on a neighbour, and today is the day you catch that. Tomorrow your suite reaches under the page, to the API that feeds it.

Before you move on

Do the practice, then mark it complete

Completion is for you. It means you produced the evidence the lesson asked for, not only that you reached the bottom of the page.