Fork me 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.

19 comments:

Luke Daley said...

Great post Rob. We'll have to make sure we link to this when we get the Geb wiki going.

Kimble said...

Geb looks really promising! Thanks for taking the time to write this!

Is there a usable non-snapshot release? I have some bad experiences using snapshot releases. It's frustrating when things that used to work suddenly breaks because a dependency has been updated.

Unknown said...

Just started using Geb, and finding it great to work with. I wrote my first spec to check a table last week. Not being sure of the best way, I ended up with a content item in the page object that returned a map for a given row, with sensible names for each cell text value.

I could then have nice compact conditions like:

accountsRow(1) == [name: 'NI', debit: '35002.00', credit: '']
accountsRow(2) == [name: 'PAYE', debit: '', credit: '0.56']

Do you think the module gives you other advantages in a case like this?

Rob said...

I guess the additional advantage of a module would be if you wanted to add methods or other richer content properties. For a simple data table a Map for each row works well, though.

Luke Daley said...

@PaulB another reason to use a module is if you need to reuse it across pages. You could also achieve that through inheritance, but that is not always practical.

Eugene Polyhaev said...

Thank you very much for the list-approach to defining modules! It is much better than the original doc examples, since developers are more familiar with lists.

Anonymous said...

Hi this is Uday,

Can u explain,How to scrape/parse a content having nested Tag structure like bellow,


a
b
c


a1
b1
c1


a2
b2
c2
d2
e2


d1
e1


d


actual problem when i am saving the scraped content to DB , due to the nested table structure, data miss placement is takes place irrespective of the respective columns.

please replay to my mail id

sivakotiuday@imomentous.info

Thank you in advance.

Anonymous said...

i mean like this.

<1>
1
2
3

<1>
21
22
23

<1>
31
32
33
34
35


24
25


4

Mauro said...

PaulB can you post your code to check the table content?

21st Century Software Solutions said...

Page Object Modeling Online Training
http://www.21cssindia.com/courses/page-object-modeling-online-training-231.html
Page Object Modeling Course Contents
Why POM and what is POM
About page objet gem and its features
Creating POM Structure
Finding elements and creating pages for application
Creating panels, dialogues and other reusable components
Creating and Identifying reusable actions and verifications in pages
Page Factory
Using pages and panels in you test cases
About watir helper and handling Ajax elements
Creating routes for your application
Passing test data from yaml files
Data Magic
Using test data with Data Magic
Creating your tests and running your tests

SanthoshVijay said...

Hi

I wants to highlight the object in the page through Geb , will that be possible

For example, during execution, i wants to highlight the Google logo ,

is that possible ?

Regards

Santhosh

21st Century Software Solutions said...

Page Object Modeling Online Training, ONLINE TRAINING – IT SUPPORT – CORPORATE TRAINING http://www.21cssindia.com/courses/page-object-modeling-online-training-231.html The 21st Century Software Solutions of India offers one of the Largest conglomerations of Software Training, IT Support, Corporate Training institute in India - +919000444287 - +917386622889 - Visakhapatnam,Hyderabad Page Object Modeling Online Training, Page Object Modeling Training, Page Object Modeling, Page Object Modeling Online Training| Page Object Modeling Training| Page Object Modeling| "Courses at 21st Century Software Solutions
Talend Online Training -Hyperion Online Training - IBM Unica Online Training - Siteminder Online Training - SharePoint Online Training - Informatica Online Training - SalesForce Online Training - Many more… | Call Us +917386622889 - +919000444287 - contact@21cssindia.com
Visit: http://www.21cssindia.com/courses.html"

Unknown said...

I understand what you bring it very meaningful and useful, thanks.
Signature:
i like play happy wheels game online and play happy wheels games full and zombie tsunami game , retrica camera , retrica , happy wheels , agario

Unknown said...

Great. This article is excellent. I have read many articles with this topic, but I have not liked. I think I have the same opinion with you. baixar facebook gratis , facebook movel , facebook descargar , whatsapp descargar , mobogenie , baixar mobogenie , whatsapp messenger

Unknown said...

This is a good idea, I would like to refer to my content development. I have a few concerns can not explain, hopefully this article can help me. descargar facebook gratis , descargar facebook gratis , baixar whatsapp gratis , descargar whatsapp apk , baixar whatsapp , baixar mobogenie , mobogenie market

Rita said...

Fiery post. Regards free tube

Unknown said...

Get real time updates of all T20 2020 matches and enjoy T20 World Cup 2020 Live Score on your smart phone no matter wherever you are.

Rahman Khan said...

Apasa pe vizionează https://clicksud.ru cel mai recent episod din in hd. Distribuim toate cele mai recente Seriale turcesti pe site-ul nostru Clicksud. https://clicksud.ru Vă permite să jucați și să vă bucurați de divertisment de la Clicksud.

sheau said...

my review here Louis Vuitton fake Bags click here to investigate replica ysl handbags browse this site replica bags buy online