Skip to content

Selenium Comparison

Stefan Ludwig edited this page May 9, 2016 · 2 revisions

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 username;
    @FindBy(id = "input_p")
    private WebElement password;
    @FindBy(id = "submit")
    private WebElement submit;

    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) {
        username.sendKeys(username);
        return this;
    }

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

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

}

WebTester

public interface LoginPage extends Page {

    @Named("username")
    @IdentifyUsing("#input_un")
    TextField username();

    @Named("password")
    @IdentifyUsing("#input_p")
    PasswordField password();

    @Named("login button")
    @IdentifyUsing(value="submit", how=Id.class)
    Button submit();

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

    /* workflows */

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

    /* actions */

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

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

    default WelcomePage clickLogin () {
        submit().click();
        return create(WelcomePage.class);
    }

}

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 are declared as interfaces and use composition to inherit traits (i.e. being a "Page") from other interfaces.
  • Another difference is us using our own @IdentifyUsing annotation instead of Selenium's @FindBy. This is mainly due to the fact that @FindBy can't be declared on methods. In addition the @IdentifyUsing annotation uses a class reference as it's howparameter which makes it much more extendable.
  • 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.
  • In native Selenium you will need to call the PageFactory to initialize a new page object. In WebTester the Page base interface will hide this mechanism and offer a simple create(Class) method.
  • Pretty much everything else is identical. There isn't much to improve on the principals of page objects them selfs.

Things Native Selenium can't do:

  • Give WebElements a human readable name in order to compensate for cryptic IDs. (@Named)
  • 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. (@PostConstruct)

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;
    LoginPage startPage;

    /* life cycle*/

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

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

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

    /* 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.
  • 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 page. 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-junit4 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 () {
        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!"));
    }

}