This is only a Test
/Blog Posts
Blog Posts
/
📃
Most Common Design Patterns in Test Automation
Most Common Design Patterns in Test Automation
📃

Most Common Design Patterns in Test Automation

Most Common Design Patterns in Test Automation

Ever wondered what if you could go beyond the classical POM in how you stucture your Test Automation code? Well look no further…

Categories of Design Patterns

Broadly, design patterns fall into three main categories:

  1. Creational Patterns
    • Focus on object creation mechanisms.
    • Examples: Singleton, Factory, Builder, Prototype, Abstract Factory.
  2. Structural Patterns
    • Focus on how classes and objects are composed to form larger structures.
    • Examples: Facade, Decorator, Proxy, Adapter, Composite, Bridge.
  3. Behavioral Patterns
    • Deal with object interaction and the delegation of responsibilities.
    • Examples: Strategy, Command, Template Method, Observer, Chain of Responsibility.

Page Object Model Patterns

1. Traditional Page Object Model

In the traditional approach, each page is represented as a class that directly encapsulates its selectors and methods.

JavaScript example:

2. Page Factory Pattern

The Page Factory Pattern abstracts element initialization—often initializing locators once (or lazily).

JavaScript example:

3. Component (or Section) Object Model

Here you extract common UI parts into reusable components (like a header) and use them within page objects.

JavaScript example:

4. Screenplay Pattern

In the Screenplay Pattern, you focus on the "actor" that performs tasks. This approach encapsulates actions (tasks) and questions (assertions).

JavaScript example:

5. Fluent (Chainable) Page Object Model

Fluent POM returns the object instance from its methods so you can chain calls together.

JavaScript example:

Creational Patterns in Test Automation

1. Factory Pattern

The Factory Pattern is used when we want to create objects without directly calling their constructors.

JavaScript example:

2. Builder Pattern

The Builder Pattern is used to create complex objects step by step, with different configurations.

JavaScript example:

Structural Patterns in Test Automation

1. Decorator Pattern

The Decorator Pattern dynamically adds new functionality to an object without modifying its code.

JavaScript example:

2. Proxy Pattern

The Proxy Pattern provides a substitute or placeholder for another object to control access.

JavaScript example:

Behavioral Patterns in Test Automation

1. Strategy Pattern

The Strategy Pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable at runtime.

JavaScript example:

2. Command Pattern

The Command Pattern encapsulates a request as an object, allowing for parameterization of different requests, queuing, or logging of requests.

JavaScript example:

Logo

Terms and Conditions

Blog

Test Management System

Created with ❤️ by Clean Cut Kft. - 2025

DiscordYouTube
class LoginPage {
  constructor(page) {
    this.page = page;
    this.usernameInput = '#username';
    this.passwordInput = '#password';
    this.loginButton = '#login';
  }

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

  async enterUsername(username) {
    await this.page.fill(this.usernameInput, username);
  }

  async enterPassword(password) {
    await this.page.fill(this.passwordInput, password);
  }

  async clickLogin() {
    await this.page.click(this.loginButton);
  }
}
class LoginPageFactory {
  constructor(page) {
    this.page = page;
    this.initElements();
  }

  initElements() {
    this.usernameInput = this.page.locator('#username');
    this.passwordInput = this.page.locator('#password');
    this.loginButton = this.page.locator('#login');
  }

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

  async login(username, password) {
    await this.usernameInput.fill(username);
    await this.passwordInput.fill(password);
    await this.loginButton.click();
  }
}
// Component
class Header {
  constructor(page) {
    this.page = page;
    this.logoutButton = '#logout';
  }

  async clickLogout() {
    await this.page.click(this.logoutButton);
  }
}

// Page Object using the Component
class LoginPageWithHeader {
  constructor(page) {
    this.page = page;
    this.header = new Header(page);
    this.usernameInput = '#username';
    this.passwordInput = '#password';
    this.loginButton = '#login';
  }

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

  async login(username, password) {
    await this.page.fill(this.usernameInput, username);
    await this.page.fill(this.passwordInput, password);
    await this.page.click(this.loginButton);
  }
}
class Actor {
  constructor(name, page) {
    this.name = name;
    this.page = page;
  }

  async attemptsTo(task) {
    await task();
  }
}

const LoginTask = {
  async withCredentials(actor, username, password) {
    await actor.page.goto('<https://example.com/login>');
    await actor.page.fill('#username', username);
    await actor.page.fill('#password', password);
    await actor.page.click('#login');
  }
};

// Usage:
const john = new Actor("John", page);
await john.attemptsTo(() => LoginTask.withCredentials(john, "john", "password123"));
class FluentLoginPage {
  constructor(page) {
    this.page = page;
  }

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

  async enterUsername(username) {
    await this.page.fill('#username', username);
    return this;
  }

  async enterPassword(password) {
    await this.page.fill('#password', password);
    return this;
  }

  async submit() {
    await this.page.click('#login');
    return this;
  }
}

// Usage:
await new FluentLoginPage(page)
  .navigate()
  .then(page => page.enterUsername("admin"))
  .then(page => page.enterPassword("pass123"))
  .then(page => page.submit());
class PageFactory {
  static createPage(pageType, page) {
    switch(pageType) {
      case 'login':
        return new LoginPage(page);
      case 'registration':
        return new RegistrationPage(page);
      case 'home':
        return new HomePage(page);
      default:
        throw new Error('Unknown page type');
    }
  }
}

// Usage:
const loginPage = PageFactory.createPage('login', page);
class UserBuilder {
  constructor() {
    this.user = {};
  }

  withName(name) {
    this.user.name = name;
    return this;
  }

  withEmail(email) {
    this.user.email = email;
    return this;
  }

  withAge(age) {
    this.user.age = age;
    return this;
  }

  withPermission(permission) {
    this.user.permission = permission;
    return this;
  }

  build() {
    return this.user;
  }
}

// Usage:
const adminUser = new UserBuilder()
  .withName('Admin User')
  .withEmail('admin@example.com')
  .withAge(30)
  .withPermission('admin')
  .build();
class LoggingPageDecorator {
  constructor(page) {
    this.page = page;
  }

  async goto(url) {
    console.log(`[Log] Navigating to: ${url}`);
    return this.page.goto(url);
  }

  async click(selector) {
    console.log(`[Log] Clicking on: ${selector}`);
    return this.page.click(selector);
  }

  async fill(selector, text) {
    console.log(`[Log] Filling ${selector} with: ${text}`);
    return this.page.fill(selector, text);
  }
}

// Usage:
const originalPage = playwright.page;
const loggingPage = new LoggingPageDecorator(originalPage);
await loggingPage.goto('<https://example.com>');
class PageProxy {
  constructor(page) {
    this.page = page;
    this.performanceData = {};
  }

  async goto(url) {
    const startTime = Date.now();
    const result = await this.page.goto(url);
    const endTime = Date.now();
    this.performanceData[url] = endTime - startTime;
    console.log(`Load time (${url}): ${this.performanceData[url]}ms`);
    return result;
  }

  async click(selector) {
    return this.page.click(selector);
  }

  performanceReport() {
    return this.performanceData;
  }
}

// Usage:
const proxy = new PageProxy(page);
await proxy.goto('<https://example.com>');
console.log(proxy.performanceReport());
// Strategy interface
class LoginStrategy {
  async login(page, username, password) {
    throw new Error('The login method must be implemented');
  }
}

// Concrete strategies
class BasicLoginStrategy extends LoginStrategy {
  async login(page, username, password) {
    await page.goto('<https://example.com/login>');
    await page.fill('#username', username);
    await page.fill('#password', password);
    await page.click('#login');
  }
}

class SSOLoginStrategy extends LoginStrategy {
  async login(page, username, password) {
    await page.goto('<https://example.com/sso-login>');
    await page.fill('#sso-email', username);
    await page.click('#continue');
    // SSO-specific steps...
  }
}

// Context
class LoginContext {
  constructor(strategy) {
    this.strategy = strategy;
  }

  async executeLogin(page, username, password) {
    await this.strategy.login(page, username, password);
  }
}

// Usage:
const basicStrategy = new BasicLoginStrategy();
const context = new LoginContext(basicStrategy);
await context.executeLogin(page, 'admin', 'admin123');

// Switching strategy
const ssoStrategy = new SSOLoginStrategy();
context.strategy = ssoStrategy;
await context.executeLogin(page, 'admin@example.com', 'admin123');
// Command interface
class UICommand {
  async execute() {
    throw new Error('The execute method must be implemented');
  }
}

// Concrete commands
class NavigateCommand extends UICommand {
  constructor(page, url) {
    super();
    this.page = page;
    this.url = url;
  }

  async execute() {
    await this.page.goto(this.url);
  }
}

class FillCommand extends UICommand {
  constructor(page, selector, text) {
    super();
    this.page = page;
    this.selector = selector;
    this.text = text;
  }

  async execute() {
    await this.page.fill(this.selector, this.text);
  }
}

class ClickCommand extends UICommand {
  constructor(page, selector) {
    super();
    this.page = page;
    this.selector = selector;
  }

  async execute() {
    await this.page.click(this.selector);
  }
}

// Command invoker
class CommandInvoker {
  constructor() {
    this.commands = [];
  }

  addCommand(command) {
    this.commands.push(command);
  }

  async executeAll() {
    for (const command of this.commands) {
      await command.execute();
    }
  }
}

// Usage:
const invoker = new CommandInvoker();
invoker.addCommand(new NavigateCommand(page, '<https://example.com/login>'));
invoker.addCommand(new FillCommand(page, '#username', 'admin'));
invoker.addCommand(new FillCommand(page, '#password', 'admin123'));
invoker.addCommand(new ClickCommand(page, '#login'));
await invoker.executeAll();