Building Scalable and Reusable Test Frameworks with Selenium and Java using Page Object Model (POM) and Design Patterns

Building Scalable and Reusable Test Frameworks with Selenium and Java using Page Object Model (POM) and Design Patterns

In the realm of software testing, having a robust and scalable test framework is crucial for maintaining software quality. By integrating the Page Object Model (POM) with Design Patterns such as Factory, Singleton, and Strategy, you can build a test framework that is both scalable and reusable. This article explores how to combine POM with these Design Patterns, providing practical examples to illustrate the approach.

What is Page Object Model (POM)?

The Page Object Model (POM) is a design pattern where each page of the application is represented by a class. This pattern separates the test logic from the page-specific logic, making the codebase more maintainable and the tests more reusable.

Advantages of POM:

  • Separation of Concerns: Isolates test logic from page-specific interactions, simplifying maintenance.
  • Reusability: Facilitates the reuse of page interactions across multiple tests.
  • Ease of Maintenance: Reduces the impact of UI changes, affecting only the Page Object classes.

Design Patterns in Test Automation

Incorporating Design Patterns with POM can significantly enhance the flexibility and maintainability of your test framework. Below are three key Design Patterns that can be effectively combined with POM:

  • Factory Pattern

Purpose: Provides a way to create objects without specifying the exact class of the object that will be created.

Example:

public class PageFactory {
    public static LoginPage getLoginPage(WebDriver driver, LoginStrategy loginStrategy) {
        return new LoginPage(driver, loginStrategy);
    }

    public static HomePage getHomePage(WebDriver driver) {
        return new HomePage(driver);
    }
}        

  • Singleton Pattern

Purpose: Ensures that a class has only one instance and provides a global access point to it.

Example:

public class DriverManager {
    private static WebDriver driver;

    private DriverManager() {}

    public static WebDriver getDriver() {
        if (driver == null) {
            driver = new ChromeDriver();
        }
        return driver;
    }

    public static void quitDriver() {
        if (driver != null) {
            driver.quit();
            driver = null;
        }
    }
}        

  • Strategy Pattern

Purpose: Defines a family of algorithms, encapsulates each one, and makes them interchangeable.

Example:

Strategy Interface:

public interface LoginStrategy {
    void login(String username, String password);
}        

Concrete Strategy for Standard Login:

public class StandardLoginStrategy implements LoginStrategy {
    private WebDriver driver;

    public StandardLoginStrategy(WebDriver driver) {
        this.driver = driver;
    }

    @Override
    public void login(String username, String password) {
        driver.findElement(By.id("username")).sendKeys(username);
        driver.findElement(By.id("password")).sendKeys(password);
        driver.findElement(By.id("login")).click();
    }
}        

Concrete Strategy for OAuth Login:

public class OAuthLoginStrategy implements LoginStrategy {
    private WebDriver driver;

    public OAuthLoginStrategy(WebDriver driver) {
        this.driver = driver;
    }

    @Override
    public void login(String username, String password) {
        driver.findElement(By.id("oauth-login")).click();
        driver.findElement(By.id("oauth-username")).sendKeys(username);
        driver.findElement(By.id("oauth-password")).sendKeys(password);
        driver.findElement(By.id("oauth-submit")).click();
    }
}        

Page Object Using Strategy:

LoginPage

public class LoginPage {
    private WebDriver driver;
    private LoginStrategy loginStrategy;

    public LoginPage(WebDriver driver, LoginStrategy loginStrategy) {
        this.driver = driver;
        this.loginStrategy = loginStrategy;
    }

    public void login(String username, String password) {
        loginStrategy.login(username, password);
    }
}        

HomePage

public class HomePage {
    private WebDriver driver;
    private By welcomeMessage = By.id("welcome");

    public HomePage(WebDriver driver) {
        this.driver = driver;
    }

    public boolean isWelcomeMessageDisplayed() {
        return driver.findElement(welcomeMessage).isDisplayed();
    }
}        

Using the Design Patterns in Tests

Here's how you can utilize the StandardLoginStrategy and OAuthLoginStrategy instances in your test cases:

  • Standard Login Test:

@Test
public void testLoginWithStandardStrategy() {
    WebDriver driver = DriverManager.getDriver();
    LoginStrategy standardLoginStrategy = new StandardLoginStrategy(driver);
    LoginPage loginPage = PageFactory.getLoginPage(driver, standardLoginStrategy);
    loginPage.login("user", "password");

    HomePage homePage = PageFactory.getHomePage(driver);
    Assert.assertTrue(homePage.isWelcomeMessageDisplayed());
    
    DriverManager.quitDriver();
}        

  • OAuth Login Test:

@Test
public void testLoginWithOAuthStrategy() {
    WebDriver driver = DriverManager.getDriver();
    LoginStrategy oauthLoginStrategy = new OAuthLoginStrategy(driver);
    LoginPage loginPage = PageFactory.getLoginPage(driver, oauthLoginStrategy);
    loginPage.login("user", "password");

    HomePage homePage = PageFactory.getHomePage(driver);
    Assert.assertTrue(homePage.isWelcomeMessageDisplayed());
    
    DriverManager.quitDriver();
}        

Benefits of This Approach

  • Scalability: The modular structure allows for easy addition of new features and test scenarios.
  • Simplified Maintenance: Changes in the application are reflected only in Page Object classes, minimizing the impact on test classes.
  • Clean and Reusable Code: The combination of POM and Design Patterns promotes clean, organized, and reusable code, facilitating collaboration and test expansion.

Conclusion

Combining the Page Object Model with Design Patterns offers a powerful approach to creating an automated test framework that is scalable, reusable, and easy to maintain. By applying these concepts, you can ensure that your test automation is effective and adaptable to future changes and expansions.

Investing time in designing a robust test automation architecture will not only improve software quality but also optimize the development and maintenance process. Leverage these techniques to transform your testing approach and enhance the effectiveness of your software projects.

Idalio Pessoa

Senior Ux Designer | Product Designer | UX/UI Designer | UI/UX Designer | Figma | Design System |

7mo

The Factory Pattern example you provided really stands out, showcasing its benefits in simplifying object creation. Well done, Daivid Simões!

Christian DeLaphante

C# Test Automation Architect | Mentor

7mo

Do not use Singleton or static webdrivers if you want your framework to be thread safe and performant!....when designing and building a framework that will need to be scalable and stable, make sure to always ensure that you make it synchronized and thread safe if you are to run tests in parallel!

Rafael ..

Software Quality Assurance Analyst | Java | Python | Selenium | BDD | Cucumber | pwd

7mo

Thanks for sharing! Always working with excellence!

Lucas Wolff

.NET Developer | C# | TDD | Angular | Azure | SQL

7mo

Great post

To view or add a comment, sign in

More articles by Daivid Simões

Insights from the community

Others also viewed

Explore topics