Fork me on GitHub

30 Nov 2010

Encapsulating page state and actions in Geb

Geb Page and Module classes are just regular Groovy classes. In addition to the content DSL you can declare methods just as you would in any other class. Those methods can use content properties directly. This is really useful for encapsulating state and actions in a reusable way.

In this post I want to run through a couple of simple examples of re-usable Module classes that are enhanced with methods. Although the examples deal with Module classes everything here is equally applicable to Page classes.

Page object methods tend to fall into two categories:

Observers
report on some aspect of page state (e.g. whether a user is logged in or not).
have non-void return types (I prefer explicit return types on methods but def would work too).
can be used for making assertions.
Actions
perform an action such as clicking a link or button.
will typically be void (note that it's not necessary to return a Page instance or class from a navigation method in Geb).
throw IllegalStateException if the action is invalid in the current page state.

First let's consider an authentication module that can be used across every page in a site. The markup of the module can be completely different depending on whether a user is logged in or not. When not logged in a username/password form is shown:

<aside id="auth"> 
    <fieldset> 
        <legend>Log In</legend> 
        <form action="/auth/login" method="post" > 
            <label>Username: <input name="username"/></label> 
            <label>Password: <input type="password" name="password"></label> 
            <button name="login" type="submit">Log In</button> 
        </form> 
    </fieldset> 
</aside> 

When logged in, a welcome message is shown:

<aside id="auth"> 
    Welcome back <span class="username">blackbeard</span> 
    <a href="/auth/logout" name="logout">Log Out</a> 
</aside>

To model this I'll create a Geb Module like this:

class AuthModule extends Module {
    static base = { $("aside#auth") }

    static content = {
        username(required: false) { $(".username").text() }
        logoutButton(required: false, to: HomePage) { $("a[name=logout]") }
        form(required: false) { $("form") }
        loginButton(required: false, to: HomePage) { $("button[name=login]") }
    }
}

The required: false declaration is used to declare content properties that may or may not be on the page. The username and logoutButton properties are only present in the logged-in state and the form and loginButton properties are only present in the logged-out state.

I can use the module in some tests like this:

def "user can log in"() {
    when:
    authModule.form.username = "blackbeard"
    authModule.form.password = "yohoho!"
    authModule.loginButton.click()

    then:
    at HomePage
    authModule.username == "blackbeard"
}

def "user can log out"() {
    when:
    authModule.logoutButton.click()

    then:
    at HomePage
    authModule.username == null
}

This is a good start but there are several assumptions and bits of page detail creeping into the test. The test is using the presence or absence of the username to determine if someone is logged in or not. That doesn't lead to a very meaningful assertion in the test and is assuming things about the page detail. Likewise the login step is quite long-winded and likely to be repeated in a lot of tests. Not only that but it won't work if a user is already logged in as the form fields won't be present on the page.

To encapsulate the module's state and actions better I'll add the following methods:

boolean isLoggedIn() { username }

void login(String username, String password = "password") {
    if (loggedIn) throw new IllegalStateException("already logged in")
    form.username = username
    form.password = password
    loginButton.click()
}

void logout() {
    if (!loggedIn) throw new IllegalStateException("already logged out")
    logoutButton.click()
}

The methods declared by the module abstract some detail away very effectively. The isLoggedIn method means I can change the login detection mechanism later and just change the module's method rather than a bunch of tests. The login and logout methods abstract away the how of logging in and out so the test can just deal with the what. I've used IllegalStateException for cases where a method is called when the module is not in the correct state. The tests now look much clearer:

def "user can log in"() {
    when:
    authModule.login "blackbeard"

    then:
    at HomePage
    authModule.username == "blackbeard"
}

def "user can log out"() {
    when:
    authModule.logout()

    then:
    at HomePage
    !authModule.loggedIn
}

Another good example of encapsulating state and behaviour like this is a typical pagination module that would appear on a list or search results page. The markup would look something like this (I've omitted the link href attributes for clarity):

<nav class="pagination">
    <a class="prevLink">Previous</a>
    <a class="step">1</a>
    <span class="currentStep">2</span>
    <a class="step">3</a>
    <a class="nextLink">Next</a>
</nav>

The previous and next links will only appear when there is a previous or next page and no links will be present at all if there is only a single page. The following Module class models the state and actions of this component:

class Pagination extends Module {
    static content = {
        links(required: false) { $("a") }
        currentPage(required: false) { $(".currentStep")?.text()?.toInteger() ?: 1 }
        nextLink(required: false) { links.filter(".nextLink") }
        previousLink(required: false) { links.filter(".prevLink") }
    }

    boolean isFirstPage() {
        previousLink.empty
    }

    boolean isLastPage() {
        nextLink.empty
    }

    void toPage(int pageNumber) {
        def link = links.filter(text: "$pageNumber")
        if (!link) throw new IllegalArgumentException("Page number $pageNumber not present in pagination")
        link.click()
    }

    void nextPage() {
        if (lastPage) throw new IllegalStateException("Already on the last page")
        nextLink.click()
    }

    void previousPage() {
        if (firstPage) throw new IllegalStateException("Already on the first page")
        previousLink.click()
    }
}

Breaking the Module down in detail:

  • The currentPage property returns the current page number as an int and defaults to 1 if there is no pagination present in the page.
  • The isFirstPage and isLastPage observer methods use the absence of the previous and next links respectively to determine if the current page is the first or last one.
  • The toPage method finds a numbered link and clicks it, throwing IllegalArgumentException if no such link is present.
  • The nextPage and previousPage action methods throw IllegalStateException if the relevant link is not on the page.

The Pagination class now neatly encapsulates the detail of the pagination elements and presents a higher-level façade to the tests.

Fuller versions of the examples in this post can be found on GitHub.

26 Nov 2010

Modelling repeating structures with Geb page objects

Geb is the hot new thing in Grails functional testing. One of its most powerful features is the concise DSL for defining page objects. The reasons for using page objects are well enumerated elsewhere but the basic point is to allow your tests to interact with pages in a manner agnostic of the detail of their structure. This is both practical (you can change markup structure without having to fix numerous tests that only fail because they were tightly coupled to that structure) and aesthetic (your tests read more like a user’s interaction with the page - the what rather than the how).

I want to put together a few short blog posts dealing with patterns that I find useful for defining page objects and modules in Geb. As I go I’ll keep adding to a very simple Grails project showing working examples which is available on GitHub.

In this post I want to talk about repeating data structures such as lists and tables and how to model them effectively.

A content property in a Geb Page or Module can be any type; whatever is returned from the defining closure. This will frequently be a Geb Navigator instance or a String but can be whatever is useful for the tests you’re writing. A good rule of thumb is that the test should be dealing with as simplified a view of the data as possible. All the complexity of traversing HTML elements and manipulating them into a useful form should be hidden away in the page objects and modules. When handling repeating data structures such as ol or table elements you probably want to be able to treat the content as a List so that tests can use Groovy features such as iterator methods, indexing and slicing to make very expressive assertions.

Simple repeating structures

For example, imagine we want to verify an ol element like this:

<ol id="recent-books">
    <li>Zero History</li>
    <li>Surface Detail</li>
    <li>The Machine of Death</li>
</ol>

The most useful type would probably be a List<String> which we can get easily enough by defining our content like this:

static content = {
    recentBooks { $("ol#recent-books li")*.text() }
}

This is then very easy to use in a test:

expect:
recentBooks == ["Zero History", "Surface Detail", "The Machine of Death"]

Complex repeating structures using Modules

A more complex example of a repeating structure is a table, where each row contains several fields. Here we can use a Geb Module to represent each row, with content properties to get data from each cell. Let’s say we want to verify the contents of the following table of search results:

<table id="book-results">
    <thead>
        <tr>
            <th>Title</th>
            <th>Author</th>
            <th>Format</th>
            <th>Price</th>
            <th>Release Date</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>Zero History</td>
            <td>William Gibson</td>
            <td>Hardback</td>
            <td>£12.29</td>
            <td>2 Sep 2010</td>
        </tr>
        <tr>
            <td>Zero History</td>
            <td>William Gibson</td>
            <td>Kindle</td>
            <td>£11.99</td>
            <td>2 Sep 2010</td>
        </tr>
        <tr>
            <td>Spook Country</td>
            <td>William Gibson</td>
            <td>Paperback</td>
            <td>£5.00</td>
            <td>31 Jul 2008</td>
        </tr>
        <tr>
            <td>Pattern Recognition</td>
            <td>William Gibson</td>
            <td>Paperback</td>
            <td>£4.99</td>
            <td>24 Jun 2004</td>
        </tr>
    </tbody>
</table>

We can define our module like this:

static content = {
    bookResults { i -> module BookRow, $("table#book-results tbody tr", i) }
}

class BookRow extends Module {
    static content = {
        cell { i -> $("td", i) }
        title { cell(0).text() }
        author { cell(1).text() }
        format { cell(2).text() }
        price { cell(3).text()[1..-1].toDouble() }
        releaseDate { cell(4).text() }
    }
}

The bookResults content closure takes a row number parameter and uses it to select the corresponding tr from the body of the table and use that to construct a module. The module itself defines content properties with meaningful names that map to the text in each cell.

This isn’t bad as far as it goes. We can use the module pretty effectively in tests like this:

expect:
bookResults(0).title == "Zero History"
bookResults(3).price == 4.99

However, bookResults isn’t a List. We can’t easily get all the book titles at once or make an assertion that all the authors are the same or find the lowest price. Even querying how many rows there are would require an additional content property or method to retrieve $("tbody tr").size(). The table is a repeating data structure and it would be nice to treat it as one!

This ought to be possible bearing in mind 3 things:

  1. The type of a content property is simply whatever you return from the defining closure, there’s no reason we can’t return a List<BookRow>.
  2. There’s nothing special about the expression that constructs the module itself: module BookRow, $("tbody tr", i) is just a call to a method called module passing a Class<? extends Module> which is the module type we want and a Navigator pointing to the module’s root element.
  3. The Geb Navigator class returned by the $ expression implements Iterable<Navigator> and can be treated like a List of all the selected HTML elements.

In fact we can get a List<BookRow> easily enough if we redefine the bookResults property like this:

static content = {
    bookResults {
        $("tbody tr").collect {
            module BookRow, it
        }
    }
}

The key here is that we iterate over the tr elements inside the content definition collecting a new BookRow instance for each one. Now the page object doesn’t require the test to pass in the index of the row it’s interested in. This enables our test to do some much more powerful and interesting things:

expect:
bookResults.size() == 4
bookResults[0].title == "Zero History"
bookResults.title.unique() == ["Zero History", "Spook Country", "Pattern Recognition"]
bookResults.every { it.author == "William Gibson" }
bookResults[2..3].every { it.format == "Paperback" }
bookResults.price.sum() == 34.27

I’ve tried to show a couple of reasonably simple examples here. Others are easy to imagine; a Map representing the dt and dd elements inside an HTML definition list, a list of modules representing a group of labelled radio buttons or news items with images and links, a tree-like multi-level navigation structure, etc.