Sunday, May 25, 2025

Key Design Patterns for Effective Test Automation

This example we will explore how to use different design pattern for effective test automation framework in playwright automation. It helps to ensure that your code is flexible, maintainable, consistent, and efficient.

Key Design Patterns for Effective Test Automation

Here are key design patterns for effective test automation with detailed explanations and code examples in Playwright (TypeScript/JavaScript):


1. Page Object Model (POM)

The Page Object Model separates test code from page-specific code like locators and actions, improving maintainability and readability.

Benefits:

  1. Reusability of code.
  2. Easier to maintain.
  3. Encapsulation of UI interactions


Project Structure:

tests/
  login.spec.ts
pages/
  LoginPage.ts


pages/LoginPage.ts

import { Page } from '@playwright/test';

export class LoginPage {
  constructor(private page: Page) {}

  async goto() {
    await this.page.goto('https://example.com/login');
  }

  async login(username: string, password: string) {
    await this.page.fill('#username', username);
    await this.page.fill('#password', password);
    await this.page.click('#login');
  }
}

tests/login.spec.ts

import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';

test('Login test', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.goto();
  await loginPage.login('admin', 'admin123');
  expect(await page.isVisible('#dashboard')).toBe(true);
});

2. Singleton Pattern for Browser/Page Instances

Ensures a single shared browser or page instance across tests (e.g., for global setup or shared state).

Benefits:

  1. Reduces overhead from multiple browser launches.
  2. Manages shared configuration/settings.

import { Browser, chromium } from '@playwright/test';

class BrowserSingleton {
  private static browser: Browser;

  static async getInstance(): Promise<Browser> {
    if (!this.browser) {
      this.browser = await chromium.launch({ headless: true });
    }
    return this.browser;
  }
}
Usage in test:
const browser = await BrowserSingleton.getInstance();
const context = await browser.newContext();
const page = await context.newPage();

3. Factory Pattern for Dynamic Page Objects

Used to create different page objects dynamically, depending on the test scenario or route.

Benefits:

  1. Useful in multi-page applications or role-based systems.
  2. Centralizes creation logic

class PageFactory {
  static create(page: Page, pageName: string) {
    switch (pageName) {
      case 'login':
        return new LoginPage(page);
      case 'dashboard':
        return new DashboardPage(page);
      default:
        throw new Error('Unknown page');
    }
  }
}
Usage in test:
const loginPage = PageFactory.create(page, 'login');
await loginPage.login('user', 'pass');

4. Test Data Builder Pattern

Encapsulates the logic for building complex test data in an easy, readable way.

Benefits:

  1. Clean test cases.
  2. Reduces duplication.

class UserBuilder {
  private username = 'testuser';
  private password = 'password123';

  withUsername(username: string) {
    this.username = username;
    return this;
  }

  withPassword(password: string) {
    this.password = password;
    return this;
  }

  build() {
    return {
      username: this.username,
      password: this.password,
    };
  }
}

Usage in test:

const user = new UserBuilder().withUsername('admin').build();
await loginPage.login(user.username, user.password);

5. Strategy Pattern for Assertion Handling

Defines a family of assertion strategies and makes them interchangeable.

Benefits:

  1. Reusable and flexible validation strategies.
  2. Clean separation of concerns.

interface ValidationStrategy {
  validate(page: Page): Promise<void>;
}

class DashboardValidation implements ValidationStrategy {
  async validate(page: Page) {
    await expect(page.locator('#dashboard')).toBeVisible();
  }
}

class ErrorValidation implements ValidationStrategy {
  async validate(page: Page) {
    await expect(page.locator('#error')).toHaveText('Invalid credentials');
  }
}

async function runTestWithValidation(
  page: Page,
  validation: ValidationStrategy
) {
  const loginPage = new LoginPage(page);
  await loginPage.goto();
  await loginPage.login('user', 'wrongpass');
  await validation.validate(page);
}

6. Component-Based Design (Reusable Widgets)

Encapsulates reusable UI elements like modals, dropdowns, or menus into separate classes.

Benefits:

  1. Reusability of UI component interactions.
  2. Cleaner tests.

class Modal {
  constructor(private page: Page) {}

  async open() {
    await this.page.click('#open-modal');
  }

  async close() {
    await this.page.click('#close-modal');
  }

  async isVisible() {
    return await this.page.isVisible('#modal');
  }
}


This is all about using key design pattern to implement effective test automation framework in playwright, cypress, JavaScript and Typescript.


No comments:

Post a Comment