Skip to content
CodeKruPro

Your language · Python

Design patterns in test code

Singleton, factory and builder, and where they show up in automation.

Learn: Design patterns in Python test code9 minute read

A design pattern is a well-known, reusable shape for solving a common design problem. In Python interviews these questions have a twist: many classic patterns need less code in Python, because functions are objects, modules (Python files) are imported once, arguments can have defaults, and pytest fixtures (functions that prepare things for a test) manage when objects are created and cleaned up. A strong answer names the pattern, the problem it solved and the Python way to write it.

Step 1: Singleton

A singleton is one shared instance (one object only), such as settings loaded once or one API client session (a connection reused for many web calls). In Python, the simplest singleton is a module-level object: a module runs once on first import and is then cached, so every from config import settings gets the same object. In pytest, a scope="session" fixture gives one instance for the whole run (one per worker under xdist, a plugin that runs tests in parallel).

A class-based version overrides __new__ (the method that creates the object) to return a stored instance. One catch: Python still calls __init__ (the method that sets up the object) on every call, so set-up placed in __init__ runs again. The trap is the same as in Java: one shared WebDriver (the Selenium object that controls a browser) breaks parallel runs (tests that run at the same time), so create a driver per test in a fixture.

Step 2: Factory

A factory hides which class gets created. In Python a plain function is usually enough: create_driver(browser) returns a configured driver, and the driver fixture calls it. A dict that maps names to functions is another compact form (a lambda is a short function written in one line):

BUILDERS = {
    "chrome": lambda: webdriver.Chrome(options=chrome_options()),
    "firefox": lambda: webdriver.Firefox(options=firefox_options()),
}

def create_driver(browser):
    try:
        return BUILDERS[browser.lower()]()
    except KeyError:
        raise ValueError(f"Unknown browser: {browser}") from None

Step 3: Builder

A builder creates an object step by step. Java builders exist largely because Java has no default or keyword arguments. In Python, a dataclass with defaults gives most of the benefit: each test sets only the fields it cares about, and dataclasses.replace makes a changed copy. A fluent builder, with methods that return self, is still useful when building is a process, such as generating a unique email for each user.

import itertools
from dataclasses import dataclass, replace

class Settings:
    _instance = None
    def __new__(cls):
        if cls._instance is None:
            print("loading settings once")
            cls._instance = super().__new__(cls)
            cls._instance.browser = "chrome"
        return cls._instance

print(Settings() is Settings(), Settings().browser)

@dataclass(frozen=True)
class User:
    name: str = "Test User"
    email: str = "test.user@example.com"
    role: str = "viewer"

admin = User(name="Asha", role="admin")
locked = replace(admin, email="locked@example.com")
print(admin)
print(locked.email, admin.email)

class UserBuilder:
    _ids = itertools.count(1)
    def __init__(self):
        self._role = "viewer"
    def with_role(self, role):
        self._role = role
        return self
    def build(self):
        n = next(self._ids)
        return User(name=f"user{n}", email=f"user{n}@example.com", role=self._role)

print(UserBuilder().with_role("admin").build())
print(UserBuilder().build().email)
loading settings once
True chrome
User(name='Asha', email='test.user@example.com', role='admin')
locked@example.com test.user@example.com
User(name='user1', email='user1@example.com', role='admin')
user2@example.com

Step 4: Page Object and Strategy

In the Page Object Model, each page or component is a class that owns its locators (the rules that tell Selenium how to find an element), as underscore class attributes, and offers actions as methods. Tests call login_page.login_as(user) and never touch locators, so a UI change is fixed in one place. Keep assertions in tests and let pages report state, such as is_error_shown().

A strategy is a piece of behaviour that can be swapped behind one interface (the same call works for every version). Because functions are objects, a Python strategy is often just a function chosen from a dict: log in through the UI or by setting an API token cookie.

def login_via_ui(user):
    return f"typed credentials for {user}"

def login_via_api(user):
    return f"set token cookie for {user}"

STRATEGIES = {"ui": login_via_ui, "api": login_via_api}

def start_session(user, how="api"):
    return STRATEGIES[how](user)

print(start_session("asha"))
print(start_session("asha", how="ui"))
set token cookie for asha
typed credentials for asha

How to answer in an interview

Name the patterns you really used, each with its problem and its file or class, and show you know the Python way: "Settings are a module-level object, the driver comes from a create_driver function inside a fixture, test users are frozen dataclasses with defaults, and pages are page objects." Then mention one thing you avoided on purpose, such as a shared driver, and why.

Common mistakes

  • Writing Java-style builder and factory classes where a dataclass or a function would do.
  • Putting one-time set-up in __init__ of a __new__ singleton.
  • Sharing one WebDriver between tests or workers.
  • Putting assertions inside page objects.
  • Listing pattern names without the problem each one solved.

Recap

A module-level object or a session fixture is the usual Python singleton. A function is usually enough for a factory or a strategy, and a dataclass with defaults replaces most builders. Page objects keep locators and actions out of tests. Each pattern earns its place only if you can name the problem it solved.

7 questions0/7 solved

StatusQuestionDifficultyAsked in
Not startedThe simplest singleton in PythonEasyAsked in: TCS
Not startedSingleton: what it is and where to use it in a Python frameworkMediumAsked in: TCS
Pro+A __new__ singleton still runs __init__MediumAsked in: TCS
Pro+Factory pattern: creating drivers in PythonMediumAsked in: TCS
Pro+Builder pattern for test data in PythonMedium
Pro+Match the pattern to the needMedium
Pro+Which design patterns have you used in your Python framework?MediumAsked in: Accenture, Capgemini, Salesforce +2
Not startedNot startedIn progressIn progressSolvedSolvedPro+Pro+

The first 2 questions of each topic are free. Pro+ opens every question.

Back to the plan