Design patterns are proven solutions to recurring problems. In test automation, four patterns appear again and again: Factory (creating objects), Strategy (swapping algorithms), Builder (constructing complex objects step by step), and Singleton (ensuring one instance). Understanding these patterns elevates your framework from a collection of scripts to a maintainable software system.
Four Essential Design Patterns for Test Automation
Each pattern solves a specific problem that every growing framework encounters.
# ── PATTERN 1: Factory — creating the right object from configuration ──
class DriverFactory:
_creators = {
"chrome": lambda opts: webdriver.Chrome(options=opts),
"firefox": lambda opts: webdriver.Firefox(options=opts),
"edge": lambda opts: webdriver.Edge(options=opts),
}
@classmethod
def register(cls, name, creator):
cls._creators[name] = creator
@classmethod
def create(cls, browser_name, **kwargs):
creator = cls._creators.get(browser_name.lower())
if not creator:
raise ValueError(f"Unknown browser: {browser_name}")
return creator(kwargs.get("options"))
# Extend without modifying: DriverFactory.register("safari", create_safari)
# ── PATTERN 2: Strategy — swapping behaviour at runtime ──
class WaitStrategy:
def wait_for(self, driver, locator, timeout):
raise NotImplementedError
class ExplicitWaitStrategy(WaitStrategy):
def wait_for(self, driver, locator, timeout):
return WebDriverWait(driver, timeout).until(
EC.visibility_of_element_located(locator)
)
class PollingWaitStrategy(WaitStrategy):
def wait_for(self, driver, locator, timeout):
end = time.time() + timeout
while time.time() < end:
elements = driver.find_elements(*locator)
if elements and elements[0].is_displayed():
return elements[0]
time.sleep(0.25)
raise TimeoutError(f"Element not found: {locator}")
class BasePage:
def __init__(self, driver, wait_strategy=None):
self.driver = driver
self.wait = wait_strategy or ExplicitWaitStrategy()
def find(self, locator, timeout=10):
return self.wait.wait_for(self.driver, locator, timeout)
# Swap strategy without changing BasePage or any PageObject:
# page = LoginPage(driver, wait_strategy=PollingWaitStrategy())
# ── PATTERN 3: Builder — constructing complex test data step by step ──
class UserBuilder:
def __init__(self):
self._data = {
"name": f"User_{int(time.time())}",
"email": f"user_{int(time.time())}@test.com",
"password": "TestPass1!",
"role": "customer",
"verified": True,
}
def with_name(self, name):
self._data["name"] = name
return self
def with_role(self, role):
self._data["role"] = role
return self
def unverified(self):
self._data["verified"] = False
return self
def build(self):
return dict(self._data)
# Usage — readable and flexible:
# admin = UserBuilder().with_name("Admin").with_role("admin").build()
# unverified = UserBuilder().unverified().build()
# default = UserBuilder().build() # sensible defaults
# ── PATTERN 4: Singleton — ensuring one shared instance ──
class ConfigManager:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._load_config()
return cls._instance
def _load_config(self):
self.base_url = os.getenv("BASE_URL", "https://staging.app.com")
self.timeout = int(os.getenv("TIMEOUT", "10"))
self.browser = os.getenv("BROWSER", "chrome")
self.headless = os.getenv("HEADLESS", "true") == "true"
# Every call returns the SAME instance:
# config1 = ConfigManager()
# config2 = ConfigManager()
# config1 is config2 → True
# Pattern summary
PATTERNS = [
("Factory", "Create the right object from a name/config", "DriverFactory, PageObjectFactory"),
("Strategy", "Swap an algorithm without changing the caller", "WaitStrategy, ReportStrategy, DataStrategy"),
("Builder", "Construct complex objects step by step", "UserBuilder, OrderBuilder, AddressBuilder"),
("Singleton", "Ensure exactly one instance of a class", "ConfigManager, DatabaseConnection, Logger"),
]
import os, time
print("Design Patterns for Test Automation")
print("=" * 70)
for name, purpose, examples in PATTERNS:
print(f"\n {name}")
print(f" Purpose: {purpose}")
print(f" Examples: {examples}")
UserBuilder().with_role("admin").build() is dramatically more readable than {"name": "User_123", "email": "...", "password": "...", "role": "admin", "verified": True}. The builder provides defaults for every field, so each test specifies only the fields that matter for that scenario — making test intent crystal clear.DriverFactory.register("safari", creator)) follows the Open/Closed Principle — new browsers are added by registering a creator function, not by modifying the factory's if/elif chain. This extensibility is what makes the pattern production-grade: teams can add custom browsers, mock drivers for unit testing, or specialised configurations without touching the factory's core logic.TestDataManager that tracks created test data across tests introduces hidden coupling — one test's data setup affects another test's expectations. Use Singletons only for read-only, immutable state (configuration, constants). For mutable state (test data, browser sessions), use per-test instances created by fixtures.Common Mistakes
Mistake 1 — Using inheritance when Strategy pattern is appropriate
❌ Wrong: ChromeBasePage, FirefoxBasePage, GridBasePage — duplicating BasePage for each variation.
✅ Correct: One BasePage that accepts a WaitStrategy or DriverFactory — behaviour varies by composition, not inheritance.
Mistake 2 — Building test data with raw dictionaries instead of Builders
❌ Wrong: Every test constructs a 10-field dictionary with all user attributes, even when only the role matters.
✅ Correct: UserBuilder().with_role("admin").build() — defaults handle the 9 irrelevant fields; the test expresses only what matters.