Strategy Pattern

Browser Strategy, Wait Strategy, Reporting Strategy

What is the Strategy Pattern?

Strategy Pattern defines a family of algorithms, puts each one in its own class, and makes them interchangeable at runtime. The client code doesn’t know which algorithm it’s using — it just calls the interface, and the right strategy does the work.

The Three Parts

┌─────────────┐        ┌─────────────────────┐
│   Context    │───────▶│   Strategy Interface │
│ (uses a      │        └──────────┬──────────┘
│  strategy)   │                   │
└─────────────┘        ┌──────────┼──────────┐
                       │          │          │
                ┌──────┴───┐ ┌───┴────┐ ┌───┴────┐
                │StrategyA │ │StrategyB│ │StrategyC│
                │ (Chrome)  │ │(Firefox)│ │ (Edge)  │
                └───────────┘ └────────┘ └────────┘
  1. Strategy Interface — defines what the algorithm does
  2. Concrete Strategies — different implementations of the algorithm
  3. Context — uses a strategy without knowing which one

Strategy vs If/Else Chains

// WITHOUT Strategy — growing if/else chains
public void takeScreenshot(String format) {
    if (format.equals("png")) {
        // 15 lines of PNG logic
    } else if (format.equals("base64")) {
        // 15 lines of base64 logic
    } else if (format.equals("file")) {
        // 15 lines of file logic
    }
    // Adding a new format? Edit this method. Violates Open/Closed principle.
}

// WITH Strategy — each format is its own class
screenshotStrategy.capture(driver, scenario);
// Adding a new format? Create a new class. No existing code changes.

Browser Strategy

Select the browser implementation at runtime without if/else chains. This is Strategy + Factory combined — a very common pattern:

The Interface

public interface BrowserStrategy {
    WebDriver createDriver();
    String getBrowserName();
}

Concrete Strategies

public class ChromeStrategy implements BrowserStrategy {
    private final boolean headless;

    public ChromeStrategy(boolean headless) {
        this.headless = headless;
    }

    @Override
    public WebDriver createDriver() {
        ChromeOptions options = new ChromeOptions();
        options.addArguments("--disable-gpu", "--no-sandbox",
            "--window-size=1920,1080");
        if (headless) options.addArguments("--headless=new");
        return new ChromeDriver(options);
    }

    @Override
    public String getBrowserName() { return "Chrome"; }
}

public class FirefoxStrategy implements BrowserStrategy {
    private final boolean headless;

    public FirefoxStrategy(boolean headless) {
        this.headless = headless;
    }

    @Override
    public WebDriver createDriver() {
        FirefoxOptions options = new FirefoxOptions();
        if (headless) options.addArguments("--headless");
        return new FirefoxDriver(options);
    }

    @Override
    public String getBrowserName() { return "Firefox"; }
}

public class EdgeStrategy implements BrowserStrategy {
    @Override
    public WebDriver createDriver() {
        return new EdgeDriver();
    }

    @Override
    public String getBrowserName() { return "Edge"; }
}

Context — DriverManager

public class DriverManager {
    private BrowserStrategy strategy;

    public DriverManager(BrowserStrategy strategy) {
        this.strategy = strategy;
    }

    // Can change strategy at runtime!
    public void setStrategy(BrowserStrategy strategy) {
        this.strategy = strategy;
    }

    public WebDriver createDriver() {
        System.out.println("Creating " + strategy.getBrowserName()
            + " driver...");
        WebDriver driver = strategy.createDriver();
        driver.manage().window().maximize();
        return driver;
    }
}

// Usage — strategy selected from config
boolean headless = ConfigReader.getInstance().isHeadless();
BrowserStrategy strategy = switch (ConfigReader.getInstance().getBrowser()) {
    case "firefox" -> new FirefoxStrategy(headless);
    case "edge"    -> new EdgeStrategy();
    default        -> new ChromeStrategy(headless);
};

DriverManager manager = new DriverManager(strategy);
WebDriver driver = manager.createDriver();

Wait Strategy

Different elements need different wait approaches. A Wait Strategy lets you swap wait logic without changing test code:

The Interface

public interface WaitStrategy {
    WebElement waitForElement(WebDriver driver, By locator);
    String getDescription();
}

Concrete Strategies

public class VisibleWaitStrategy implements WaitStrategy {
    private final Duration timeout;

    public VisibleWaitStrategy(Duration timeout) {
        this.timeout = timeout;
    }

    @Override
    public WebElement waitForElement(WebDriver driver, By locator) {
        return new WebDriverWait(driver, timeout)
            .until(ExpectedConditions.visibilityOfElementLocated(locator));
    }

    @Override
    public String getDescription() { return "Wait until visible"; }
}

public class ClickableWaitStrategy implements WaitStrategy {
    private final Duration timeout;

    public ClickableWaitStrategy(Duration timeout) {
        this.timeout = timeout;
    }

    @Override
    public WebElement waitForElement(WebDriver driver, By locator) {
        return new WebDriverWait(driver, timeout)
            .until(ExpectedConditions.elementToBeClickable(locator));
    }

    @Override
    public String getDescription() { return "Wait until clickable"; }
}

public class PresenceWaitStrategy implements WaitStrategy {
    private final Duration timeout;

    public PresenceWaitStrategy(Duration timeout) {
        this.timeout = timeout;
    }

    @Override
    public WebElement waitForElement(WebDriver driver, By locator) {
        return new WebDriverWait(driver, timeout)
            .until(ExpectedConditions.presenceOfElementLocated(locator));
    }

    @Override
    public String getDescription() { return "Wait until present in DOM"; }
}

Usage in Page Objects

public class BasePage {
    protected WebDriver driver;
    protected WaitStrategy waitStrategy;

    public BasePage(WebDriver driver, WaitStrategy waitStrategy) {
        this.driver = driver;
        this.waitStrategy = waitStrategy;
    }

    // All page methods use the strategy
    protected WebElement find(By locator) {
        return waitStrategy.waitForElement(driver, locator);
    }

    protected void click(By locator) {
        find(locator).click();
    }

    protected void type(By locator, String text) {
        WebElement element = find(locator);
        element.clear();
        element.sendKeys(text);
    }
}

// Login page uses clickable strategy for buttons
LoginPage loginPage = new LoginPage(driver,
    new ClickableWaitStrategy(Duration.ofSeconds(10)));

// Dashboard uses visible strategy for elements that load dynamically
DashboardPage dashPage = new DashboardPage(driver,
    new VisibleWaitStrategy(Duration.ofSeconds(15)));

Locator Strategy

Elements can be found by ID, CSS, XPath, etc. A Locator Strategy tries multiple approaches, providing fallback resilience:

public interface LocatorStrategy {
    WebElement findElement(WebDriver driver, String identifier);
}

// Try ID first, then CSS, then XPath
public class ResilientLocatorStrategy implements LocatorStrategy {

    @Override
    public WebElement findElement(WebDriver driver, String identifier) {
        // Try ID first (fastest)
        try {
            return driver.findElement(By.id(identifier));
        } catch (NoSuchElementException e) { /* fall through */ }

        // Try CSS selector
        try {
            return driver.findElement(By.cssSelector(identifier));
        } catch (NoSuchElementException e) { /* fall through */ }

        // Try XPath as last resort
        try {
            return driver.findElement(By.xpath(identifier));
        } catch (NoSuchElementException e) {
            throw new NoSuchElementException(
                "Element not found with any strategy: " + identifier
            );
        }
    }
}

// Strict — only uses one locator type
public class CssSelectorStrategy implements LocatorStrategy {
    @Override
    public WebElement findElement(WebDriver driver, String cssSelector) {
        return driver.findElement(By.cssSelector(cssSelector));
    }
}

// Data-attribute strategy (best for test automation)
public class DataTestIdStrategy implements LocatorStrategy {
    @Override
    public WebElement findElement(WebDriver driver, String testId) {
        return driver.findElement(
            By.cssSelector("[data-testid='" + testId + "']")
        );
    }
}

Reporting Strategy

Swap reporting implementations without changing test code. Your framework can generate Allure reports locally and Extent reports for stakeholders:

public interface ReportingStrategy {
    void startTest(String testName);
    void logStep(String description, String status);
    void attachScreenshot(byte[] screenshot, String name);
    void endTest(String status);
    void flush();
}

public class ExtentReportStrategy implements ReportingStrategy {
    private final ExtentReports extent;
    private final ThreadLocal<ExtentTest> test = new ThreadLocal<>();

    public ExtentReportStrategy(String reportPath) {
        extent = new ExtentReports();
        extent.attachReporter(
            new ExtentSparkReporter(reportPath)
        );
    }

    @Override
    public void startTest(String testName) {
        test.set(extent.createTest(testName));
    }

    @Override
    public void logStep(String description, String status) {
        if ("pass".equalsIgnoreCase(status)) {
            test.get().pass(description);
        } else {
            test.get().fail(description);
        }
    }

    @Override
    public void attachScreenshot(byte[] screenshot, String name) {
        test.get().addScreenCaptureFromBase64String(
            Base64.getEncoder().encodeToString(screenshot), name
        );
    }

    @Override
    public void endTest(String status) {
        // Extent auto-handles status based on logged steps
    }

    @Override
    public void flush() {
        extent.flush();
    }
}

public class ConsoleReportStrategy implements ReportingStrategy {
    @Override
    public void startTest(String testName) {
        System.out.println("═══ START: " + testName + " ═══");
    }

    @Override
    public void logStep(String description, String status) {
        System.out.println("  [" + status.toUpperCase() + "] "
            + description);
    }

    @Override
    public void attachScreenshot(byte[] screenshot, String name) {
        System.out.println("  [SCREENSHOT] " + name + " ("
            + screenshot.length + " bytes)");
    }

    @Override
    public void endTest(String status) {
        System.out.println("═══ END: " + status.toUpperCase() + " ═══\n");
    }

    @Override
    public void flush() { /* nothing to flush */ }
}

Usage in Hooks

public class Hooks {
    private static ReportingStrategy reporter;

    @BeforeAll
    public static void initReporter() {
        String reportType = ConfigReader.getInstance()
            .get("report.type", "console");

        reporter = switch (reportType) {
            case "extent" -> new ExtentReportStrategy(
                "target/reports/extent-report.html");
            case "allure" -> new AllureReportStrategy();
            default       -> new ConsoleReportStrategy();
        };
    }

    @Before
    public void beforeScenario(Scenario scenario) {
        reporter.startTest(scenario.getName());
    }

    @After
    public void afterScenario(Scenario scenario) {
        reporter.endTest(scenario.getStatus().toString());
    }

    @AfterAll
    public static void flushReport() {
        reporter.flush();
    }

    public static ReportingStrategy getReporter() {
        return reporter;
    }
}
# Switch reporters via config — no code change!
mvn test -Dreport.type=extent
mvn test -Dreport.type=allure
mvn test -Dreport.type=console

Environment Strategy

Different environments (dev, staging, prod) have different URLs, credentials, and configurations. Encapsulate each as a strategy:

public interface EnvironmentStrategy {
    String getBaseUrl();
    String getApiBaseUrl();
    String getAdminUser();
    String getAdminPassword();
    int getTimeout();
}

public class DevEnvironment implements EnvironmentStrategy {
    public String getBaseUrl()       { return "https://dev.example.com"; }
    public String getApiBaseUrl()    { return "https://api-dev.example.com"; }
    public String getAdminUser()     { return "dev_admin"; }
    public String getAdminPassword() { return "devPass123"; }
    public int getTimeout()          { return 5; }
}

public class StagingEnvironment implements EnvironmentStrategy {
    public String getBaseUrl()       { return "https://staging.example.com"; }
    public String getApiBaseUrl()    { return "https://api-staging.example.com"; }
    public String getAdminUser()     { return "stage_admin"; }
    public String getAdminPassword() { return "stagePass456"; }
    public int getTimeout()          { return 10; }
}

public class ProdEnvironment implements EnvironmentStrategy {
    public String getBaseUrl()       { return "https://www.example.com"; }
    public String getApiBaseUrl()    { return "https://api.example.com"; }
    public String getAdminUser()     { return "prod_admin"; }
    public String getAdminPassword() { return "prodPass789"; }
    public int getTimeout()          { return 15; }
}

// Factory to pick the right strategy
public class EnvironmentFactory {
    public static EnvironmentStrategy getEnvironment() {
        String env = System.getProperty("env", "staging");
        return switch (env) {
            case "dev"  -> new DevEnvironment();
            case "prod" -> new ProdEnvironment();
            default     -> new StagingEnvironment();
        };
    }
}

// Usage
EnvironmentStrategy env = EnvironmentFactory.getEnvironment();
driver.get(env.getBaseUrl() + "/login");
loginPage.login(env.getAdminUser(), env.getAdminPassword());
# Run against different environments
mvn test -Denv=dev
mvn test -Denv=staging
mvn test -Denv=prod

When to Use & When Not To

Strategy Pattern in Your Framework — Checklist

ComponentStrategy?Reason
Browser selectionYes3+ browsers with different options and setup
Wait mechanismYesDifferent waits per element type
ReportingYesSwap between Allure/Extent/Console
Environment configYesDev/staging/prod with different URLs and data
Screenshot formatMaybeOnly if you need 3+ formats
Assertion styleNoUse assertion libraries directly

All Three Patterns Together

In a real framework, Factory, Singleton, and Strategy work together:

// SINGLETON — Config loaded once, used everywhere
ConfigReader config = ConfigReader.getInstance();

// STRATEGY — Environment selected at runtime
EnvironmentStrategy env = EnvironmentFactory.getEnvironment();

// FACTORY — Creates the right driver based on config
WebDriver driver = DriverFactory.createDriver(config.getBrowser());

// STRATEGY — Reporter selected at runtime
ReportingStrategy reporter = switch (config.get("report.type")) {
    case "extent" -> new ExtentReportStrategy("target/report.html");
    default       -> new ConsoleReportStrategy();
};

// All working together
driver.get(env.getBaseUrl());
reporter.startTest("Login Test");
reporter.logStep("Navigated to " + env.getBaseUrl(), "pass");
// ...

Summary