Skip to content
CodeKruPro

Your language · Java

Design patterns in test code

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

Learn: Design patterns in Java test code9 minute read

A design pattern is a well-known, reusable shape for solving a common design problem. "Which design patterns have you used?" is a favourite framework question, and interviewers do not want a list of names. They want the problem each pattern solved in your own code. (A framework here means the project of shared test code that all your tests use.) Five patterns cover almost every Java test framework: Singleton, Factory, Builder, Page Object and Strategy.

Step 1: Singleton

A Singleton class has exactly one instance and one global way to reach it. Typical uses are a configuration reader that loads a file once, a report manager or a connection pool. A private constructor stops other code from calling new, and a static method returns the shared object.

The instance must be created safely when several threads (parts of a program that run at the same time) ask at the same time. Safe versions are an eager static final field, the holder idiom (a nested class whose static field is created on first use, shown in the sample below) and an enum with one constant. A plain if (instance == null) check without locking can create two objects.

The trap is a singleton WebDriver (WebDriver is the Selenium object that controls a browser). One shared browser breaks parallel runs and lets state leak from test to test, so use one driver per test thread instead.

Step 2: Factory

A factory hides which class gets created. The caller passes a simple value and gets back an interface type (a type that lists methods without saying how they work), so tests depend only on that interface. The everyday example is a driver factory that returns Chrome, Firefox or a remote driver from a configuration value, with all browser options in one place.

public static WebDriver create(String browser) {
    return switch (browser.toLowerCase()) {
        case "chrome" -> new ChromeDriver();
        case "firefox" -> new FirefoxDriver();
        default -> throw new IllegalArgumentException("Unknown browser: " + browser);
    };
}

Step 3: Builder and Strategy

A builder creates an object step by step with named methods and finishes with build(). It replaces constructors with long argument lists, gives sensible defaults, and usually returns an object that cannot change. Test data then reads like the test case: "a user with role admin". REST Assured's given().header(...).body(...) (REST Assured is a Java library for API tests) uses the same chained style, where each call returns the object so the next call can follow.

A strategy is a piece of behaviour that can be swapped behind one interface. Logging in through the UI or with an API token can be two strategies with the same login method, so a test picks one without changing its steps. With lambdas (short functions; see Lambdas and streams), a strategy can be as small as one functional interface (an interface with one method).

import java.util.*;

final class Config {
    private static final class Holder { static final Config INSTANCE = new Config(); }
    private final Map<String, String> values = new HashMap<>();
    private Config() {
        System.out.println("loading config once");
        values.put("browser", "chrome");
    }
    static Config get() { return Holder.INSTANCE; }
    String value(String key) { return values.get(key); }
}

final class User {
    private final String name, email, role;
    private User(Builder b) { name = b.name; email = b.email; role = b.role; }
    static Builder builder() { return new Builder(); }
    @Override public String toString() { return name + " <" + email + "> " + role; }

    static final class Builder {
        private String name = "Test User";
        private String email = "test.user@example.com";
        private String role = "viewer";
        Builder name(String v) { name = v; return this; }
        Builder role(String v) { role = v; return this; }
        User build() { return new User(this); }
    }
}

interface LoginStrategy { String login(User user); }

public class Main {
    public static void main(String[] args) {
        System.out.println(Config.get().value("browser"));
        System.out.println(Config.get() == Config.get());

        User admin = User.builder().name("Asha").role("admin").build();
        User viewer = User.builder().build();
        System.out.println(admin);
        System.out.println(viewer);

        Map<String, LoginStrategy> strategies = Map.of(
            "ui", u -> "typed credentials for " + u.toString().split(" ")[0],
            "api", u -> "set token cookie for " + u.toString().split(" ")[0]);
        System.out.println(strategies.get("api").login(admin));
    }
}
loading config once
chrome
true
Asha <test.user@example.com> admin
Test User <test.user@example.com> viewer
set token cookie for Asha

"loading config once" prints a single time even though Config.get() is called three times, because the holder class is set up on first use only.

Step 4: Page Object Model

The Page Object Model is not one of the classic patterns from the "Gang of Four" book (the well-known 1994 book on design patterns), but it is the most important one in UI automation. Each page or component is a class that owns its locators (the rules that tell Selenium how to find an element on the page; they are private) and offers actions (public), such as loginPage.loginAs(user). When the UI changes, you fix one class instead of fifty tests. Page methods can return the next page object, so a test reads like the steps a user takes. Keep assertions in the tests, not in page objects, so pages stay reusable.

How to answer in an interview

Name three or four patterns you really used. For each, say the problem, the class that implements it and the benefit: "DriverFactory keeps the browser choice in one place; ConfigReader is a singleton so the file loads once; a User builder keeps test data short." Then mention one thing you avoided on purpose, such as a singleton driver, and why. Be ready to open the code, because follow-up questions go straight into it.

Common mistakes

  • Listing pattern names without the problem each one solved.
  • Making WebDriver a singleton in a suite that runs in parallel.
  • Writing a lazy singleton (created on first use) with no thread safety.
  • Putting assertions inside page objects.
  • Mixing up a factory (which class to create) with a builder (how to set up one object).

Recap

Singleton gives one shared instance, and the holder idiom or an enum makes it thread-safe. Factory chooses which class to create, Builder sets up one object step by step, and Strategy swaps behaviour behind an interface. Page Object keeps locators and actions out of tests. Never share one driver across parallel tests.

7 questions0/7 solved

StatusQuestionDifficultyAsked in
Not startedWhat is the Singleton pattern?EasyAsked in: TCS
Not startedSingleton: what it is and where to use it in a frameworkMediumAsked in: TCS
Pro+Which singletons are thread-safe?MediumAsked in: TCS
Pro+Factory pattern: a DriverFactoryMediumAsked in: TCS
Pro+Builder pattern for test dataMedium
Pro+Match the pattern to the needMedium
Pro+Which design patterns have you used in your 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