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) │
└───────────┘ └────────┘ └────────┘
- Strategy Interface — defines what the algorithm does
- Concrete Strategies — different implementations of the algorithm
- 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
| Component | Strategy? | Reason |
|---|---|---|
| Browser selection | Yes | 3+ browsers with different options and setup |
| Wait mechanism | Yes | Different waits per element type |
| Reporting | Yes | Swap between Allure/Extent/Console |
| Environment config | Yes | Dev/staging/prod with different URLs and data |
| Screenshot format | Maybe | Only if you need 3+ formats |
| Assertion style | No | Use 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");
// ...