How to implement WebDriver PageObject methods that can return different PageObjects

19,926

Solution 1

Bohemian's answer is not flexible - you cannot have a page action returning you to the same page (such as entering a bad password), nor can you have more than 1 page action resulting in different pages (think what a mess you'd have if the Login page had another action resulting in different outcomes). You also end up with heaps more PageObjects just to cater for different results.

After trialing this some more (and including the failed login scenario), I've settled on the following:

private <T> T login(String user, String pw, Class<T> expectedPage){
    username.sendKeys(user);
    password.sendKeys(pw);
    submitButton.click();
    return PageFactory.initElements(driver, expectedPage);
}

public AdminWelcome loginAsAdmin(String user, String pw){
    return login(user, pw, AdminWelcome.class);
}

public CustomerWelcome loginAsCustomer(String user, String pw){
    return login(user, pw, CustomerWelcome.class);
}

public Login loginWithBadCredentials(String user, String pw){
    return login(user, pw, Login.class);
}

This means you can reuse the login logic, but prevent the need for the test class to pass in the expected page, which means the test class is very readable:

Login login = PageFactory.initElements(driver, Login.class);
login = login.loginWithBadCredentials("bad", "credentials");
// TODO assert login failure message
CustomerWelcome customerWelcome = login.loginAsCustomer("joe", "smith");
// TODO do customer things

Having separate methods for each scenario also makes the Login PageObject's API very clear - and it's very easy to tell all of the outcomes of logging in. I didn't see any value in using interfaces to restrict the pages used with the login() method.

I'd agree with Tom Anderson that reusable WebDriver code should be refactored into fine-grained methods. Whether they are exposed finely-grained (so the test class can pick and choose the relevant operations), or combined and exposed to the test class as a single coarsely-grained method is probably a matter of personal preference.

Solution 2

You are polluting your API with multiple types - just use generics and inheritance:

public abstract class Login<T> {

    @FindBy(id = "username")
    private WebElement username;

    @FindBy(id = "password")
    private WebElement password;

    @FindBy(id = "submitButton")
    private WebElement submitButton;

    private WebDriver driver;

    private Class<T> clazz;

    protected Login(WebDriver driver, Class<T> clazz) {
        this.driver = driver;
        this.clazz = clazz
    }

    public T login(String user, String pw){
        username.sendKeys(user);
        password.sendKeys(pw);
        submitButton.click();
        return PageFactory.initElements(driver, clazz);
    }
}

and then

public AdminLogin extends Login<AdminWelcome> {

   public AdminLogin(WebDriver driver) {
       super(driver, AdminWelcome.class);
   }
}

public CustomerLogin extends Login<CustomerWelcome> {

   public CustomerLogin(WebDriver driver) {
       super(driver, CustomerWelcome.class);
   }
}

etc for all types on login pages


Note the work-around for type erasure of being able to pass an instance of Class<T> to the PageFactory.initElements() method, by passing an instance of the class into the constructor, which is known as the "type token" pattern.

Share:
19,926

Related videos on Youtube

James Bassett
Author by

James Bassett

Previously known on SO as Hound Dog

Updated on September 15, 2022

Comments

  • James Bassett
    James Bassett over 1 year

    I've just started using WebDriver, and I'm trying to learn the best practices, in particular using PageObjects and PageFactory.

    It's my understanding that PageObjects should expose the various operations on a web page, and isolate the WebDriver code from the test class. Quite often, the same operation can result in navigating to different pages depending on the data used.

    For example, in this hypothetical Login scenario, providing admin credentials takes you to the AdminWelcome page, and providing Customer credentials takes you to the CustomerWelcome page.

    So the easiest way to implement this is to expose two methods which return different PageObjects...

    Login PageObject

    package example;
    
    import org.openqa.selenium.WebDriver;
    import org.openqa.selenium.WebElement;
    import org.openqa.selenium.support.FindBy;
    import org.openqa.selenium.support.PageFactory;
    
    public class Login {
    
        @FindBy(id = "username")
        private WebElement username;
    
        @FindBy(id = "password")
        private WebElement password;
    
        @FindBy(id = "submitButton")
        private WebElement submitButton;
    
        private WebDriver driver;
    
        public Login(WebDriver driver){
            this.driver = driver;
        }
    
        public AdminWelcome loginAsAdmin(String user, String pw){
            username.sendKeys(user);
            password.sendKeys(pw);
            submitButton.click();
            return PageFactory.initElements(driver, AdminWelcome.class);
        }
    
        public CustomerWelcome loginAsCustomer(String user, String pw){
            username.sendKeys(user);
            password.sendKeys(pw);
            submitButton.click();
            return PageFactory.initElements(driver, CustomerWelcome.class);
        }
    
    }
    

    And do the following in the test class:

    Login loginPage = PageFactory.initElements(driver, Login.class);
    AdminWelcome adminWelcome = loginPage.loginAsAdmin("admin", "admin");
    

    or

    Login loginPage = PageFactory.initElements(driver, Login.class);
    CustomerWelcome customerWelcome = loginPage.loginAsCustomer("joe", "smith");
    

    Alternative approach

    Instead of duplicating code, I was hoping there was a cleaner way of exposing a single login() method which returned the relevant PageObject.

    I thought about creating a hierarchy of pages (or having them implement an interface) so that I could use that as the return type, but it feels clumsy. What I came up with was the following:

    public <T> T login(String user, String pw, Class<T> expectedPage){
        username.sendKeys(user);
        password.sendKeys(pw);
        submitButton.click();
        return PageFactory.initElements(driver, expectedPage);
    }
    

    Which means you can do the following in the test class:

    Login loginPage = PageFactory.initElements(driver, Login.class);
    AdminWelcome adminWelcome = 
        loginPage.login("admin", "admin", AdminWelcome.class);
    

    or

    Login loginPage = PageFactory.initElements(driver, Login.class);
    CustomerWelcome customerWelcome = 
        loginPage.login("joe", "smith", CustomerWelcome.class);
    

    This is flexible - you could add an ExpiredPassword page and not have to change the login() method at all - just add another test and pass in the appropriate expired credentials and the ExpiredPassword page as the expected page.

    Of course, you could quite easily leave the loginAsAdmin() and loginAsCustomer() methods and replace their contents with a call to the generic login() (which would then be made private). A new page (e.g. the ExpiredPassword page) would then require another method (e.g. loginWithExpiredPassword()).

    This has the benefit that the method names actually mean something (you can easily see that there are 3 possible results of logging in), the PageObject's API is a bit easier to use (no 'expected page' to pass in), but the WebDriver code is still being reused.

    Further improvements...

    If you did expose the single login() method, you could make it more obvious which pages can be reached from logging in by adding a marker interface to those pages (this is probably not necessary if you expose a method for each scenario).

    public interface LoginResult {}
    
    public class AdminWelcome implements LoginResult {...}
    
    public class CustomerWelcome implements LoginResult {...}
    

    And update the login method to:

    public <T extends LoginResult> T login(String user, String pw, 
        Class<T> expectedPage){
        username.sendKeys(user);
        password.sendKeys(pw);
        submitButton.click();
        return PageFactory.initElements(driver, expectedPage);
    }
    

    Either approach seems to work well, but I'm not sure how it would scale for more complicated scenarios. I haven't seen any code examples like it, so I'm wondering what everyone else does when actions on a page can result in different outcomes depending on the data?

    Or is it common practice to just duplicate the WebDriver code and expose lots of different methods for each permutation of data/PageObjects?

    • Tom Anderson
      Tom Anderson over 11 years
      When i hit this problem, i went with your first solution of having different methods - enterValidCreditCardNumber and enterInvalidCreditCardNumber, submitFormSuccessfully and submitFormUnsuccessfully, etc. There is some duplication, but it does leave a very straightforward, readable, test.
  • James Bassett
    James Bassett over 11 years
    That's quite good, but might be limiting if there was another operation on the Login page that could result in a different set of PageObjects (with inheritance you're locked in to 1). Oh, and your Login constructor is missing this.clazz = clazz :)
  • James Bassett
    James Bassett over 11 years
    Also, is PageObject a custom class/interface, or is part of the WebDriver/Selenium related APIs? I removed that when testing your idea, as my PageObjects don't extend/implement anything.
  • Bohemian
    Bohemian over 11 years
    Sorry - I guessed that PageObject was an interface... but it's not, it's a concept, so it's just <T> not <T exyends PageObject>. Also fixed constructor
  • James Bassett
    James Bassett over 11 years
    This answer really answers another question - how to write generic methods without supplying the desired class to return. I'm really after the best practices for writing PageObject methods that can return different results. But thanks anyway :)
  • djangofan
    djangofan over 11 years
    I am not understanding how this answer doesn't answer your question.
  • James Bassett
    James Bassett over 11 years
    @djangofan For the reasons I gave in the comments above - by doing it this way you can only ever have 1 action that results in different pages. I'm after a pattern that works in all scenarios (i.e. WebDriver best practices). I'll update my question to make this clearer.
  • djangofan
    djangofan over 11 years
    Ok. I am also very interested in solving this problem. Any interest in sharing a GitHub project?
  • James Bassett
    James Bassett over 11 years
    I've posted my own answer - it seems to work quite well. I'll try and put together a GitHub example, that'd be quite useful :)
  • Bohemian
    Bohemian over 11 years
    @djangofan I would be interested in contributing to a github project. Let me know if you want my help. WebDriver seems to need a lot of glue code and boilerplate code... not very productive. It would be good to create a framework that provided a simple and powerful façade to all that low-value work
  • djangofan
    djangofan over 11 years
    I've already been thinking about a framework. I have some basic stuff here if you want to fork it (with me as upstream) and then invite me to share on your modification: github.com/djangofan/WebDriverTestingTemplate Or, alternatively, start a separate project and add me as a github friend/collaborator.