Design test components with single, well-defined responsibilities
Create extensible frameworks without modifying existing code
Design proper inheritance hierarchies in page object models
Create focused interfaces for test utilities and services
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.
"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.
// ❌ 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 }
// ✅ 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: Identify SRP violations: List all the responsibilities in the class Create abstractions: Define interfaces for each responsibility Implement OCP: Make the solution extensible Apply LSP: Ensure proper inheritance hierarchies Use ISP: Create focused interfaces 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. Module 2 Complete! 🎉
"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.
// 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: Identify SRP violations: List all the responsibilities in the class Create abstractions: Define interfaces for each responsibility Implement OCP: Make the solution extensible Apply LSP: Ensure proper inheritance hierarchies Use ISP: Create focused interfaces 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. Module 2 Complete! 🎉
// 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: Identify SRP violations: List all the responsibilities in the class Create abstractions: Define interfaces for each responsibility Implement OCP: Make the solution extensible Apply LSP: Ensure proper inheritance hierarchies Use ISP: Create focused interfaces 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. Module 2 Complete! 🎉
"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 - 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: Identify SRP violations: List all the responsibilities in the class Create abstractions: Define interfaces for each responsibility Implement OCP: Make the solution extensible Apply LSP: Ensure proper inheritance hierarchies Use ISP: Create focused interfaces 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. Module 2 Complete! 🎉
// ✅ 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: Identify SRP violations: List all the responsibilities in the class Create abstractions: Define interfaces for each responsibility Implement OCP: Make the solution extensible Apply LSP: Ensure proper inheritance hierarchies Use ISP: Create focused interfaces 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. Module 2 Complete! 🎉
"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.
// ❌ 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: Identify SRP violations: List all the responsibilities in the class Create abstractions: Define interfaces for each responsibility Implement OCP: Make the solution extensible Apply LSP: Ensure proper inheritance hierarchies Use ISP: Create focused interfaces 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. Module 2 Complete! 🎉
// ✅ 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: Identify SRP violations: List all the responsibilities in the class Create abstractions: Define interfaces for each responsibility Implement OCP: Make the solution extensible Apply LSP: Ensure proper inheritance hierarchies Use ISP: Create focused interfaces 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. Module 2 Complete! 🎉
"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.
// ❌ 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: Identify SRP violations: List all the responsibilities in the class Create abstractions: Define interfaces for each responsibility Implement OCP: Make the solution extensible Apply LSP: Ensure proper inheritance hierarchies Use ISP: Create focused interfaces 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. Module 2 Complete! 🎉
// ✅ 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: Identify SRP violations: List all the responsibilities in the class Create abstractions: Define interfaces for each responsibility Implement OCP: Make the solution extensible Apply LSP: Ensure proper inheritance hierarchies Use ISP: Create focused interfaces 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. Module 2 Complete! 🎉
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: Identify SRP violations: List all the responsibilities in the class Create abstractions: Define interfaces for each responsibility Implement OCP: Make the solution extensible Apply LSP: Ensure proper inheritance hierarchies Use ISP: Create focused interfaces 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. Module 2 Complete! 🎉
Each class should have one reason to change. Separate concerns into focused components.
Design for extension without modification. Use abstractions and polymorphism.
Subclasses must be substitutable for their base classes without breaking functionality.
Create focused, specific interfaces rather than large, monolithic ones.
Depend on abstractions, not concretions. Use dependency injection for flexibility.
SOLID principles lead to more maintainable, testable, and extensible automation frameworks.