Automation Architecture Course > Module 2: SOLID Principles in Test Automation

Module 2: SOLID Principles in Test Automation

⏱️ 60 minutes 📈 Advanced 🎯 Design Principles

🎯 Learning Objectives

Apply Single Responsibility Principle

Design test components with single, well-defined responsibilities

Implement Open/Closed Principle

Create extensible frameworks without modifying existing code

Use Liskov Substitution

Design proper inheritance hierarchies in page object models

Apply Interface Segregation

Create focused interfaces for test utilities and services

🏗️ SOLID Principles in Test Automation Context

The SOLID principles, originally designed for object-oriented programming, are incredibly powerful when applied to test automation. They help create maintainable, extensible, and robust automation frameworks that can evolve with your application and team needs.

💡 Why SOLID Matters in Test Automation

  • Maintainability: Changes in one area don't break unrelated tests
  • Extensibility: Easy to add new features without major refactoring
  • Testability: Components can be easily unit tested
  • Team Collaboration: Clear responsibilities make parallel development easier
S

Single Responsibility Principle (SRP)

"A class should have only one reason to change."

In test automation: Each test class, page object, or utility should have a single, well-defined purpose.

❌ SRP Violation Example

// ❌ This class has too many responsibilities
public class LoginPageTest {
    private WebDriver driver;
    // Responsibility 1: Test execution
    @Test
    public void testLogin() {
        navigateToLoginPage();
        enterCredentials("user", "pass");
        clickLoginButton();
        validateLogin();
        generateReport();
        sendEmailNotification();
    }
    // Responsibility 2: Page interactions
    private void navigateToLoginPage() {
        driver.get("https://example.com/login");
    private void enterCredentials(String username, String password) {
        driver.findElement(By.id("username")).sendKeys(username);
        driver.findElement(By.id("password")).sendKeys(password);
    // Responsibility 3: Validation logic
    private void validateLogin() {
        WebElement welcomeMsg = driver.findElement(By.className("welcome"));
        Assert.assertTrue(welcomeMsg.isDisplayed());
    // Responsibility 4: Reporting
    private void generateReport() {
        // Report generation logic
    // Responsibility 5: Notifications
    private void sendEmailNotification() {
        // Email sending logic
}

✅ SRP Compliant Solution

// ✅ Each class has a single responsibility
// Responsibility: Page interactions only
public class LoginPage {
    public LoginPage(WebDriver driver) {
        this.driver = driver;
    public void navigateTo() {
    public void enterUsername(String username) {
    public void enterPassword(String password) {
    public DashboardPage clickLoginButton() {
        driver.findElement(By.id("loginBtn")).click();
        return new DashboardPage(driver);
}
// Responsibility: Dashboard page interactions only
public class DashboardPage {
    public DashboardPage(WebDriver driver) {
    public boolean isWelcomeMessageDisplayed() {
        return driver.findElement(By.className("welcome")).isDisplayed();
// Responsibility: Test execution only
public class LoginTest {
    private LoginPage loginPage;
    private TestReporter reporter;
    private NotificationService notificationService;
    public void testSuccessfulLogin() {
        loginPage.navigateTo();
        loginPage.enterUsername("user");
        loginPage.enterPassword("pass");
        DashboardPage dashboard = loginPage.clickLoginButton();
        Assert.assertTrue(dashboard.isWelcomeMessageDisplayed());
// Responsibility: Reporting only
public class TestReporter {
    public void generateReport(TestResult result) {
// Responsibility: Notifications only
public class NotificationService {
    public void sendTestCompletionEmail(TestSummary summary) {
            
                    
O

Open/Closed Principle (OCP)

"Software entities should be open for extension but closed for modification."

In test automation: Design frameworks that can be extended with new functionality without modifying existing code.

✅ OCP Implementation: Extensible Test Reporting

// Base reporter interface - closed for modification
public interface TestReporter {
    void reportTestStart(String testName);
    void reportTestResult(TestResult result);
    void reportTestSuite(TestSuiteResult suiteResult);
// Base implementation - closed for modification
public abstract class BaseTestReporter implements TestReporter {
    protected String formatTimestamp(Date timestamp) {
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(timestamp);
    protected String formatDuration(long durationMs) {
        return String.format("%.2f seconds", durationMs / 1000.0);
// Extensions - open for extension
public class ConsoleReporter extends BaseTestReporter {
    @Override
    public void reportTestResult(TestResult result) {
        System.out.println(String.format("[%s] %s - %s (%s)",
            formatTimestamp(result.getTimestamp()),
            result.getTestName(),
            result.getStatus(),
            formatDuration(result.getDuration())
        ));
public class HtmlReporter extends BaseTestReporter {
        // Generate HTML report
        String html = String.format(
            "<tr><td>%s</td><td>%s</td><td>%s</td></tr>",
        );
        writeToHtmlFile(html);
public class SlackReporter extends BaseTestReporter {
    public void reportTestSuite(TestSuiteResult suiteResult) {
        String message = String.format(
            "Test Suite: %s\nPassed: %d, Failed: %d, Duration: %s",
            suiteResult.getSuiteName(),
            suiteResult.getPassedCount(),
            suiteResult.getFailedCount(),
            formatDuration(suiteResult.getTotalDuration())
        sendToSlack(message);
// Reporter manager - can use any reporter without modification
public class ReporterManager {
    private List<TestReporter> reporters = new ArrayList<>();
    public void addReporter(TestReporter reporter) {
        reporters.add(reporter);
    public void reportResult(TestResult result) {
        reporters.forEach(reporter -> reporter.reportTestResult(result));
                

✅ OCP Implementation: Extensible Driver Management

// Base driver factory - closed for modification
public abstract class WebDriverFactory {
    public abstract WebDriver createDriver();
    protected void setCommonCapabilities(DesiredCapabilities capabilities) {
        capabilities.setCapability("acceptSslCerts", true);
        capabilities.setCapability("acceptInsecureCerts", true);
// Extensions for different browsers - open for extension
public class ChromeDriverFactory extends WebDriverFactory {
    public WebDriver createDriver() {
        ChromeOptions options = new ChromeOptions();
        options.addArguments("--disable-web-security");
        options.addArguments("--disable-features=VizDisplayCompositor");
        DesiredCapabilities capabilities = new DesiredCapabilities();
        setCommonCapabilities(capabilities);
        options.merge(capabilities);
        return new ChromeDriver(options);
public class FirefoxDriverFactory extends WebDriverFactory {
        FirefoxOptions options = new FirefoxOptions();
        options.addPreference("security.tls.insecure_fallback_hosts", "localhost");
        return new FirefoxDriver(options);
// New browser support can be added without modifying existing code
public class EdgeDriverFactory extends WebDriverFactory {
        EdgeOptions options = new EdgeOptions();
        return new EdgeDriver(options);
            
                    
L

Liskov Substitution Principle (LSP)

"Objects of a superclass should be replaceable with objects of a subclass without breaking the application."

In test automation: Subclasses should be able to replace their parent classes without changing the correctness of the program.

❌ LSP Violation Example

// ❌ LSP violation - subclass changes expected behavior
public class BasePage {
    protected WebDriver driver;
    public BasePage(WebDriver driver) {
    public void navigateTo(String url) {
        driver.get(url);
        waitForPageLoad();
    protected void waitForPageLoad() {
        // Wait for page to load
        WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
        wait.until(webDriver -> ((JavascriptExecutor) webDriver)
            .executeScript("return document.readyState").equals("complete"));
public class LoginPage extends BasePage {
        super(driver);
        // ❌ Violation: Changes the contract by not calling waitForPageLoad
        // Intentionally not calling waitForPageLoad() - breaks LSP
// This will fail because LoginPage doesn't behave like BasePage
public void testNavigation() {
    BasePage page = new LoginPage(driver);  // Should work with any BasePage
    page.navigateTo("https://example.com");
    // Expects page to be loaded, but LoginPage doesn't wait
    WebElement element = driver.findElement(By.id("username")); // May fail
                

✅ LSP Compliant Solution

// ✅ LSP compliant - all subclasses maintain the contract
public abstract class BasePage {
    protected WebDriverWait wait;
        this.wait = new WebDriverWait(driver, Duration.ofSeconds(10));
        waitForPageSpecificElements();
    // Template method - subclasses can customize without breaking contract
    protected abstract void waitForPageSpecificElements();
    public abstract boolean isPageLoaded();
    protected void waitForPageSpecificElements() {
        // Wait for login-specific elements
        wait.until(ExpectedConditions.presenceOfElementLocated(By.id("username")));
        wait.until(ExpectedConditions.presenceOfElementLocated(By.id("password")));
    public boolean isPageLoaded() {
        return driver.findElement(By.id("username")).isDisplayed() &&
               driver.findElement(By.id("password")).isDisplayed();
public class DashboardPage extends BasePage {
        // Wait for dashboard-specific elements
        wait.until(ExpectedConditions.presenceOfElementLocated(By.className("dashboard")));
        return driver.findElement(By.className("dashboard")).isDisplayed();
// Now any BasePage can be substituted without breaking functionality
    BasePage page = new LoginPage(driver);  // Or any other BasePage subclass
    Assert.assertTrue(page.isPageLoaded());  // Will work for any subclass
            
                    
I

Interface Segregation Principle (ISP)

"Clients should not be forced to depend on interfaces they do not use."

In test automation: Create focused, specific interfaces rather than large, monolithic ones.

❌ ISP Violation Example

// ❌ Fat interface - forces clients to implement methods they don't need
public interface TestUtilities {
    // Database operations
    void connectToDatabase();
    void executeQuery(String sql);
    void closeDatabase();
    // File operations
    void readFromFile(String path);
    void writeToFile(String path, String content);
    void deleteFile(String path);
    // API operations
    Response sendGetRequest(String url);
    Response sendPostRequest(String url, String body);
    void validateResponse(Response response);
    // UI operations
    void takeScreenshot();
    void highlightElement(WebElement element);
    void scrollToElement(WebElement element);
    // Reporting operations
    void generateReport();
    void sendEmailReport();
    void uploadToCloud();
// ❌ Classes forced to implement methods they don't use
public class DatabaseTestHelper implements TestUtilities {
    // Only needs database methods but forced to implement everything
    public void connectToDatabase() { /* Implementation */ }
    public void executeQuery(String sql) { /* Implementation */ }
    public void closeDatabase() { /* Implementation */ }
    // ❌ Forced to implement methods not needed
    public void readFromFile(String path) { 
        throw new UnsupportedOperationException("Not needed");
    public Response sendGetRequest(String url) { 
    // ... many more unnecessary implementations
                

✅ ISP Compliant Solution

// ✅ Segregated interfaces - focused and specific
public interface DatabaseOperations {
public interface FileOperations {
public interface ApiOperations {
public interface UiOperations {
public interface ReportingOperations {
// ✅ Classes implement only what they need
public class DatabaseTestHelper implements DatabaseOperations {
    public void connectToDatabase() {
        // Implementation
    public void executeQuery(String sql) {
    public void closeDatabase() {
public class ApiTestHelper implements ApiOperations {
    public Response sendGetRequest(String url) {
    public Response sendPostRequest(String url, String body) {
    public void validateResponse(Response response) {
// ✅ Classes can implement multiple focused interfaces if needed
public class ComprehensiveTestHelper implements DatabaseOperations, FileOperations {
    // Implements only the interfaces it actually needs
    public void readFromFile(String path) { /* Implementation */ }
    public void writeToFile(String path, String content) { /* Implementation */ }
    public void deleteFile(String path) { /* Implementation */ }
            
                    
D

Dependency Inversion Principle (DIP)

"High-level modules should not depend on low-level modules. Both should depend on abstractions."

In test automation: Depend on interfaces and abstractions, not concrete implementations.

❌ DIP Violation Example

// ❌ High-level class depends on low-level concrete classes
public class TestExecutor {
    private ChromeDriver driver;           // ❌ Depends on concrete class
    private MySQLDatabase database;        // ❌ Depends on concrete class
    private FileLogger logger;             // ❌ Depends on concrete class
    public TestExecutor() {
        // ❌ Tightly coupled to specific implementations
        this.driver = new ChromeDriver();
        this.database = new MySQLDatabase();
        this.logger = new FileLogger();
    public void executeTest(String testName) {
        logger.log("Starting test: " + testName);
        // Test execution logic
        driver.get("https://example.com");
        // Database validation
        database.connect();
        String result = database.query("SELECT * FROM users");
        database.disconnect();
        logger.log("Test completed: " + testName);
                

✅ DIP Compliant Solution

// ✅ Depend on abstractions
public interface WebDriverProvider {
    WebDriver getDriver();
    void quitDriver();
public interface DatabaseProvider {
    void connect();
    String executeQuery(String sql);
    void disconnect();
public interface Logger {
    void log(String message);
    void error(String message);
// ✅ High-level class depends on abstractions
    private final WebDriverProvider driverProvider;
    private final DatabaseProvider databaseProvider;
    private final Logger logger;
    // ✅ Dependencies injected through constructor
    public TestExecutor(WebDriverProvider driverProvider, 
                       DatabaseProvider databaseProvider, 
                       Logger logger) {
        this.driverProvider = driverProvider;
        this.databaseProvider = databaseProvider;
        this.logger = logger;
        WebDriver driver = driverProvider.getDriver();
        databaseProvider.connect();
        String result = databaseProvider.executeQuery("SELECT * FROM users");
        databaseProvider.disconnect();
// ✅ Concrete implementations
public class ChromeDriverProvider implements WebDriverProvider {
    public WebDriver getDriver() {
        if (driver == null) {
            driver = new ChromeDriver();
        return driver;
    public void quitDriver() {
        if (driver != null) {
            driver.quit();
            driver = null;
public class MySQLDatabaseProvider implements DatabaseProvider {
    private Connection connection;
    public void connect() {
        // MySQL connection logic
    public String executeQuery(String sql) {
        // MySQL query execution
        return "result";
    public void disconnect() {
        // MySQL disconnection logic
public class ConsoleLogger implements Logger {
    public void log(String message) {
        System.out.println("[INFO] " + message);
    public void error(String message) {
        System.err.println("[ERROR] " + message);
// ✅ Easy to test with mocks and easy to change implementations
public class TestExecutorTest {
    public void testExecuteTest() {
        // ✅ Can easily inject mocks for testing
        WebDriverProvider mockDriverProvider = mock(WebDriverProvider.class);
        DatabaseProvider mockDatabaseProvider = mock(DatabaseProvider.class);
        Logger mockLogger = mock(Logger.class);
        TestExecutor executor = new TestExecutor(
            mockDriverProvider, 
            mockDatabaseProvider, 
            mockLogger
        executor.executeTest("sample test");
        verify(mockLogger).log("Starting test: sample test");
        verify(mockLogger).log("Test completed: sample test");
            
                

🔧 Practical Refactoring Exercise

🎯 Exercise: Refactor Legacy Test Class

Take the following legacy test class and refactor it to follow all SOLID principles:

// Legacy code to refactor
public class UserManagementTest {
    private WebDriver driver = new ChromeDriver();
    public void testUserCreation() {
        // Navigate to user management page
        driver.get("https://example.com/admin/users");
        // Fill user form
        driver.findElement(By.id("firstName")).sendKeys("John");
        driver.findElement(By.id("lastName")).sendKeys("Doe");
        driver.findElement(By.id("email")).sendKeys("john@example.com");
        driver.findElement(By.id("role")).sendKeys("Admin");
        driver.findElement(By.id("submitBtn")).click();
        // Validate in UI
        WebElement successMsg = driver.findElement(By.className("success"));
        Assert.assertTrue(successMsg.isDisplayed());
        // Validate in database
        try {
            Connection conn = DriverManager.getConnection(
                "jdbc:mysql://localhost:3306/testdb", "user", "pass");
            Statement stmt = conn.createStatement();
            ResultSet rs = stmt.executeQuery(
                "SELECT * FROM users WHERE email='john@example.com'");
            Assert.assertTrue(rs.next());
            conn.close();
        } catch (SQLException e) {
            e.printStackTrace();
        // Generate report
            FileWriter writer = new FileWriter("test-report.html");
            writer.write("<html><body>Test Passed</body></html>");
            writer.close();
        } catch (IOException e) {
        // Send notification
            Properties props = new Properties();
            props.put("mail.smtp.host", "smtp.gmail.com");
            Session session = Session.getDefaultInstance(props);
            MimeMessage message = new MimeMessage(session);
            message.setSubject("Test Completed");
            Transport.send(message);
        } catch (MessagingException e) {
                    

Refactoring Steps:

  1. Identify SRP violations: List all the responsibilities in the class
  2. Create abstractions: Define interfaces for each responsibility
  3. Implement OCP: Make the solution extensible
  4. Apply LSP: Ensure proper inheritance hierarchies
  5. Use ISP: Create focused interfaces
  6. Apply DIP: Inject dependencies through abstractions

🎯 Key Takeaways

🎯 Single Responsibility

Each class should have one reason to change. Separate concerns into focused components.

🔓 Open/Closed

Design for extension without modification. Use abstractions and polymorphism.

🔄 Liskov Substitution

Subclasses must be substitutable for their base classes without breaking functionality.

🎛️ Interface Segregation

Create focused, specific interfaces rather than large, monolithic ones.

🔄 Dependency Inversion

Depend on abstractions, not concretions. Use dependency injection for flexibility.

🏗️ Better Architecture

SOLID principles lead to more maintainable, testable, and extensible automation frameworks.