Automation Technique · Mid-Level & Senior SDET

Test Automation Design Patterns

Design patterns are reusable solutions to recurring automation problems. Knowing them prevents you from reinventing the wheel — and from writing test code that nobody can maintain six months later.

Mid-Level Senior SDET ISTQB CTAL-TAE

1 The Hook

An SDET team writes 400 tests over 6 months. No design patterns. Every test directly selects elements. When the app redesigns the navigation, 280 tests break. Every test needs individual fixing. Three weeks of work.

If they'd used Page Object Model, only the nav page object would need updating. One file changed, 280 tests passing again in an hour. The cost of skipping patterns isn't paid when you write the tests — it's paid every time something changes.

2 The Rule

Apply the right pattern before you write the tests, not after 400 of them break. Test code is production code — it needs the same design discipline.

3 The Analogy

Analogy

Design patterns are like standardised fittings in plumbing.

Every plumber uses the same thread sizes, connector types, and valve patterns. Not because there's only one way to join pipes, but because standardised patterns mean any plumber can pick up where another left off, and any part is replaceable. Test automation patterns work the same way — a new SDET joins the team and can navigate the codebase immediately because they recognise the patterns.

4 Watch Me Do It

Five patterns with TypeScript/Playwright examples:

1. Page Object Model (POM)

Encapsulate page selectors and interactions in a class. Tests call methods, not selectors. When the UI changes, update the page object — not the tests.

// pages/LoginPage.ts
export class LoginPage {
  constructor(private page: Page) {}

  async login(username: string, password: string) {
    await this.page.fill('[data-testid="username"]', username);
    await this.page.fill('[data-testid="password"]', password);
    await this.page.click('[data-testid="login-btn"]');
  }
}

// tests/login.spec.ts — no selectors, only intent
test('valid login redirects to dashboard', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.login('tester@resync.nz', 'Test1234!');
  await expect(page).toHaveURL('/dashboard');
});

2. Screenplay Pattern

Actors perform Tasks using Interactions. More verbose than POM but better at modelling complex multi-step user journeys. Use when POM objects grow too large to manage.

// tasks/SubmitRatesPayment.ts
export const SubmitRatesPayment = (amount: number) => ({
  performAs: async (actor: Actor) => {
    await actor.attemptsTo(
      Navigate.to('/rates/pay'),
      Enter.theValue(amount).into(RatesPage.amountField),
      Click.on(RatesPage.reviewButton),
      Click.on(RatesPage.confirmButton)
    );
  }
});

// test reads like a user story
await actor.attemptsTo(SubmitRatesPayment(450.00));

3. Builder Pattern for Test Data

Fluent interface for constructing test fixtures. Avoids sprawling factory functions and makes test intent readable.

// builders/CustomerBuilder.ts
export class CustomerBuilder {
  private data = { name: 'Default User', region: 'Auckland', kiwisaver: false };

  withName(name: string)         { this.data.name = name; return this; }
  inRegion(region: string)       { this.data.region = region; return this; }
  withKiwiSaver()                { this.data.kiwisaver = true; return this; }
  build()                        { return { ...this.data }; }
}

// Test reads what matters, ignores the rest
const customer = new CustomerBuilder()
  .inRegion('Wellington')
  .withKiwiSaver()
  .build();

4. Custom Assertion / Matcher

Extend the assertion library with domain-specific matchers. Makes failing assertion messages meaningful to the team.

// matchers/toBeValidNZBankAccount.ts
expect.extend({
  toBeValidNZBankAccount(received: string) {
    const pattern = /^\d{2}-\d{4}-\d{7}-\d{2,3}$/;
    return {
      pass: pattern.test(received),
      message: () =>
        `Expected "${received}" to be a valid NZ bank account (xx-xxxx-xxxxxxx-xx)`
    };
  }
});

// Usage — failure message is self-documenting
expect(account.number).toBeValidNZBankAccount();

5. Factory Pattern for Page Objects

Centralise page object instantiation. Avoids scattering new LoginPage(page) across every test file, and makes swapping implementations easy.

// pages/PageFactory.ts
export class PageFactory {
  constructor(private page: Page) {}

  loginPage()    { return new LoginPage(this.page); }
  dashboardPage(){ return new DashboardPage(this.page); }
  paymentPage()  { return new PaymentPage(this.page); }
}

// Test fixture — one factory, consistent construction
test.beforeEach(async ({ page }, testInfo) => {
  testInfo.factory = new PageFactory(page);
});
When to use which pattern
PatternUse whenAvoid when
POMStandard UI automation — always a good defaultPage objects grow to 500+ lines; consider Screenplay
ScreenplayComplex multi-actor user journeys; BDD teamsSimple CRUD apps — overkill, adds verbosity
BuilderComplex test data with many optional fieldsSimple fixtures with 2–3 fields — direct object is fine
Custom AssertionDomain-specific validation appears frequentlyOne-off checks — inline assertion is simpler
FactoryMany page objects, multiple test suitesSmall test suite with 2–3 page objects

5 When to Apply Patterns

  • From day 1 — POM is not optional overhead; it's the minimum viable structure for any UI automation suite over 20 tests.
  • When test data setup is repetitive — Builder pattern. If you're copying object literals across tests, you're creating maintenance debt.
  • When assertions repeat domain logic — Custom Matchers. NZ bank account format, IRD number format, date range validation — write it once.
  • When a new SDET joins and asks "where do I create a page object?" — Factory. That question reveals you don't have one yet.

6 Common Mistakes

❌ I used to think: design patterns are for production code, not tests.

Actually: test code has the same maintenance burden as production code — sometimes higher. Tests change when requirements change and when the UI changes. Without patterns, every requirement change ripples through hundreds of test files. With POM, one page object changes.

❌ I used to think: I'll refactor into patterns later.

Actually: refactoring 400 tests into POM is a 2-week project. Applying POM from test 1 takes 20 minutes. The longer you wait, the more expensive it gets. "We'll add patterns when the suite is bigger" is how you end up with the 3-week repair job from the hook.

❌ I used to think: Screenplay is better than POM — I'll use it for everything.

Actually: Screenplay adds significant structural complexity that pays off in large, multi-actor user journeys. For a simple login-and-verify test suite, POM is cleaner and faster to write. Match pattern complexity to problem complexity.

7 Now You Try

🧪 Prompt Lab

You're setting up a Playwright test suite for an NZ insurance claim submission portal. Tests will cover: logging in, submitting a claim, checking claim status, and downloading a settlement letter. For each of the 5 patterns above, decide: (1) whether it applies to this scenario, (2) what it would look like, and (3) which pattern gives the biggest maintainability win here and why.

8 Self-Check

Click each question to reveal the answer.

Q1: A navigation redesign breaks 280 of 400 tests. What pattern would have prevented this, and how?

Page Object Model. All selectors for the navigation live in a NavComponent page object. When the navigation changes, you update one file — the NavComponent. All 280 tests that use it automatically inherit the fix because they call page object methods, not raw selectors. The tests describe intent; the page object describes implementation.

Q2: What is the key difference between POM and Screenplay pattern?

POM organises tests around pages — a LoginPage has methods for actions on that page. Screenplay organises tests around actors performing tasks — an Actor performs a LoginTask using an Interaction. Screenplay is more verbose but scales better for complex multi-actor scenarios (e.g. testing a workflow where a GP submits a referral and a specialist receives it). POM is the right default for most teams.

Q3: When should you write a Custom Matcher instead of an inline assertion?

When the same domain-specific validation appears in 3 or more tests. NZ IRD number format, NZ bank account format, or NZ date range validation are good candidates — they're domain rules that belong in one place. An inline assertion is fine once; a custom matcher is right when the same logic would otherwise be copied across multiple tests.

9 ISTQB Mapping

ISTQB CTAL-TAE (Test Automation Engineering) — Section 4.2 (Automation code design patterns), Section 5.2 (Test data management), Section 6.3 (Verification of automation code quality). TAE candidates are expected to select and apply appropriate design patterns based on the automation context.

POM is explicitly referenced in CTAL-TAE. Screenplay is an evolution of POM that TAE practitioners encounter in advanced teams.