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:
- Creational Patterns
- Focus on object creation mechanisms.
- Examples: Singleton, Factory, Builder, Prototype, Abstract Factory.
- Structural Patterns
- Focus on how classes and objects are composed to form larger structures.
- Examples: Facade, Decorator, Proxy, Adapter, Composite, Bridge.
- 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:
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);
}
}
2. Page Factory Pattern
The Page Factory Pattern abstracts element initialization—often initializing locators once (or lazily).
JavaScript example:
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();
}
}
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:
// 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);
}
}
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:
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"));
5. Fluent (Chainable) Page Object Model
Fluent POM returns the object instance from its methods so you can chain calls together.
JavaScript example:
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());
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:
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);
2. Builder Pattern
The Builder Pattern is used to create complex objects step by step, with different configurations.
JavaScript example:
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();
Structural Patterns in Test Automation
1. Decorator Pattern
The Decorator Pattern dynamically adds new functionality to an object without modifying its code.
JavaScript example:
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>');
2. Proxy Pattern
The Proxy Pattern provides a substitute or placeholder for another object to control access.
JavaScript example:
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());
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:
// 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');
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:
// 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();