Skip to content
This repository has been archived by the owner on Mar 10, 2020. It is now read-only.

Selenium Comparison

Stefan Ludwig edited this page Nov 25, 2015 · 1 revision

On this page we will compare WebTester's approach to page objects with Selenium's "native" page objects.

Page Object Code

Lets take a look at how page objects are defined:

Selenium

public class LoginPage {

    @FindBy ( id = "input_un" )
    private WebElement usernameField;
    @FindBy ( id = "input_p" )
    private WebElement passwordField;
    @FindBy ( id = "submit" )
    private WebElement loginButton;

    private WebDriver driver;

    public LoginPage (WebDriver driver) {
        this.driver = driver;
		assertThat(driver.getTitle(), is("TestApp: Login"));
    }

    /* workflows */

    public WelcomePage login (String username, String password) {
        return setUsername(username).setPassword(password).clickLogin();
    }

    /* actions */

    public LoginPage setUsername (String username) {
        usernameField.sendKeys(username);
        return this;
    }

    public LoginPage setPassword (String password) {
        passwordField.sendKeys(password);
        return this;
    }

    public WelcomePage clickLogin () {
        loginButton.click();
        return PageFactory.initElements(driver, WelcomePage.class);
    }

}

WebTester

public class LoginPage extends PageObject { // (1)

    @IdentifyUsing ( "input_un" ) // (2)
    private TextField usernameField; // (3)
    @IdentifyUsing ( "input_p" )
    private PasswordField passwordField;
    @IdentifyUsing ( method = Method.ID, value = "submit", name = "login button" )
    private Button loginButton;

    @PostConstruct // (4)
    public void assertThatCorrectPageIsDisplayed () {
        assertThat(getBrowser().getPageTitle(), is("TestApp: Login"));
    }

    /* workflows */  // (5)

    public WelcomePage login (String username, String password) {
        return setUsername(username).setPassword(password).clickLogin();
    }

    /* actions */

    public LoginPage setUsername (String username) {
        usernameField.setText(username);
        return this;
    }

    public LoginPage setPassword (String password) {
        passwordField.setText(password);
        return this;
    }

    public WelcomePage clickLogin () {
        loginButton.click();
        return create(WelcomePage.class); // (6)
    }

}

For the first comparison between WebTester and "native" Selenium let's take a look at how page objects are structured:

  • As you can see our page objects use inheritance in order to get rid of unwanted information like the WebDriver reference or the need for a constructor. This also means that every "page" you model is also, in its core, a WebElement wrapped as a PageObject. (more on this here)
  • Another difference is us using our own IdentifyUsing annotation instead of Selenium's FindBy. [FindBy is also supported] The reasons for this are two fold:
    • First, we wanted to be able to extend on the ways we identify elements of a page (alternative By implementations).
    • Second, we wanted to be able to give our elements human readable names (used by events to describe the object in a more understandable fashion then it's identifier).
  • Instead of everything being a WebElement we wanted to have types of elements. So there are text fields, buttons, checkboxes and much more. All of them offer specialized functions according to what they are.
  • In order to assert that (or wait until) your are displaying the correct page for a page object to handle any method (as long as it returns void and takes no parameters) can be annotated with @PostConstruct. These methods will be called after the page object is initialized and "ready" to use.
  • Pretty much everything else is identical. There isn't much to improve on the principals of page objects them selfs.
  • Except for how page objects are created. In native Selenium you will need to call the PageFactory to initialize a new page object. In WebTester the PageObject base class will hide the actual mechanism from you. This not only allows for customization of the underlying mechanisms but also hides an irrelevant detail from the page object author.

Things Native Selenium can't do:

  • Give WebElements a human readable name in order to compensate for cryptic IDs.
  • Nesting page objects into one another as "page fragments".
  • Automatically asserting the state of an element field: Since the only "automatic" way of checking the state of a displayed page is to add assertions to the constructor the WebElements will not have been initialized yet. You'll have to create a public method which then must be called by the test code in order to decouple creation from verification.

Test Code

Now lets take a look at how this impacts your test code:

Selenium

public class LoginTest  {

    static WebDriver webDriver;
    LoginPage startPage;

    /* life cycle*/

    @BeforeClass
    public static void initWebDriver () {
        webDriver = new FirefoxDriver();
    }

    @Before
    public void initStartPage () {
        webDriver.get("http://localhost:8080/login");
        startPage = PageFactory.initElements(webDriver, LoginPage.class);
    }

    @AfterClass
    public static void closeBrowser () {
        webDriver.quit();
    }

    /* tests */

    @Test
    public void testValidLogin () {
        WelcomePage page = startPage.login("username", "123456");
        assertThat(page.getWelcomeMessage(), is("Hello World!"));
    }

    @Test
    public void testInvalidLogin_Password () {
        LoginPage page = startPage.loginExpectingError("username", "bar");
        assertThat(page.getErrorMessage(), is("Wrong Credentials!"));
    }

}

WebTester

public class LoginTest  {

    static Browser browser; // (1)
    LoginPage startPage;

    /* life cycle*/

    @BeforeClass
    public static void initBrowser () {
        browser = new FirefoxFactory().createBrowser(); // (2)
    }

    @Before
    public void initStartPage () {
        startPage = browser.open("http://localhost:8080/login", LoginPage.class); // (3)
    }

    @AfterClass
    public static void closeBrowser () {
        browser.close() // (4)
    }

    /* tests */

    @Test
    public void testValidLogin () {
        WelcomePage page = startPage.login("username", "123456");
        assertThat(page.getWelcomeMessage(), is("Hello World!"));
    }

    @Test
    public void testInvalidLogin_Password () {
        LoginPage page = startPage.loginExpectingError("username", "bar");
        assertThat(page.getErrorMessage(), is("Wrong Credentials!"));
    }

}

For the second comparison let's take a look at what tests must do in order to use page objects:

  • Instead of using the WebDriver directly we wrapped it inside of a Browser object (actually an interface). This allows us to improve upon the WebDriver's API and add shortcut methods for the most used features. Shortcut in this case means a single method call for things that would take multiple lines of code to accomplish using native Selenium. (for more see here)
  • We use a factory pattern to create Browser instances by implementing the BrowserFactory interface. The reason behind this is very simple: We have never encountered a project where it was not necessary to tweak the WebDriver by providing custom properties, profiles, locations etc. The use of factories allows the developer to hide all of that logic cleanly behind a well defined pattern. You could also initialize the Browser using a BrowserBuilder which is used by the factories and offers even more tweaking options.
  • Page object are created by the browser (more precisely by a service used by the browser), which feels natural - since the real browser is displaying these pages to us. There are several ways to initialize a PageObject. The method shown below is just the most convenient for the given scenario.
  • Minor but not without difference: The Browser's close() method will close all windows and quit the driver in one method call.

WebTester with JUnit Support

The following example uses the webtester-support-junit module to get rid of some boiler plate life cycle code.

@RunWith(WebTesterJUnitRunner.class) 
public class LoginTest  {

    @Resource
    @CreateUsing(FirefoxFactory.class)
    @EntryPoint("http://localhost:8080/login")
    static Browser browser;

    LoginPage startPage;

    /* life cycle*/

    @Before
    public void initStartPage () {
		// "start pages" will soon be automatically createable (v0.9.8)
        startPage = browser.create(LoginPage.class);
    }

    /* tests */

    @Test
    public void testValidLogin () {
        WelcomePage page = startPage.login("username", "123456");
        assertThat(page.getWelcomeMessage(), is("Hello World!"));
    }

    @Test
    public void testInvalidLogin_Password () {
        LoginPage page = startPage.loginExpectingError("username", "bar");
        assertThat(page.getErrorMessage(), is("Wrong Credentials!"));
    }

}
Clone this wiki locally