Browser Automation and Acceptance Testing with Geb - codecentric AG Blog

:

This post focuses on the technical side of automated acceptance tests for web applications. There are a lot of high-level frameworks, that allow definition of acceptance tests in natural language (Robot, JBehave, Cucumber, …). But when it comes to the technical implementation of the test cases, you are often forced to use the rather low-level WebDriver API directly.

Geb addresses exactly this problem. It is an abstraction of the WebDriver API and combines the expressive and concise Groovy language with a jQuery-like selection and traversal API. This makes the test implementation easier and the code more readable. On top of that, you get support for the page object pattern, asynchronous content lookup and a very good integration in existing test frameworks.

Quickstart with the Groovy Shell

If you want to start playing around with Geb immediately, just open up a Groovy Shell (normally via groovysh) and type in the following lines. Of course, you can also save these lines to a file and execute it with groovy. But for trying out new things, I think the Groovy Shell is a good starting point.

import groovy.grape.Grape
Grape.grab(group:"org.gebish", module:"geb-core", version:"0.9.0-RC-1")
Grape.grab(group:"org.seleniumhq.selenium", module:"selenium-firefox-driver", version:"2.28.0")
import geb.Browser
 
browser = new Browser() 
 
// the duckduckgo website has a much cleaner structure than google
// that makes it a better choice for demonstrating browser automation
browser.go "https://duckduckgo.com/"

import groovy.grape.Grape Grape.grab(group:"org.gebish", module:"geb-core", version:"0.9.0-RC-1") Grape.grab(group:"org.seleniumhq.selenium", module:"selenium-firefox-driver", version:"2.28.0") import geb.Browserbrowser = new Browser()// the duckduckgo website has a much cleaner structure than google // that makes it a better choice for demonstrating browser automation browser.go "https://duckduckgo.com/"

After loading the necessary libraries with Grape, a new Browser instance is created. At this point, nothing else happens. When calling the go-method, a firefox opens up and navigates to “http://duckduckgo.com”. In the background Geb searches for available WebDriver implementations. It will find the previously loaded FirefoxDriver and will use it to start a new browser and connect to it. Now we can control the firefox via the Browser instance. For example, we can search for the term “Groovy Browser Automation” and click on the first result.

// fill the search input field with the searchterm 
// ".q" is just a shortcut for find("input", name:"q")
browser.find("#search_form_homepage").q = "Groovy Browser Automation"
 
// click the button with id "search_button_homepage"
browser.find("#search_button_homepage").click()
 
// click the first "a"-tag below the element with class "links_main" 
browser.find(".links_main").find("a", 0).click()

// fill the search input field with the searchterm // ".q" is just a shortcut for find("input", name:"q") browser.find("#search_form_homepage").q = "Groovy Browser Automation"// click the button with id "search_button_homepage" browser.find("#search_button_homepage").click()// click the first "a"-tag below the element with class "links_main" browser.find(".links_main").find("a", 0).click()

In this little example, we already see one of Geb’s core features: the jQuery-like syntax for selecting elements. You can use a great variety of CSS-selectors and attribute-matchers, you can filter the result-set or find descendants. And you can easily iterate over the result-set (like we do in the next example). For more information on element selection and traversal, have a look at Geb’s reference documentation (the “The Book of Geb”). It is not only a complete overview of Geb’s features (including configuration and integration in test frameworks and build systems), but also a good starting point for anybody who wants to learn more about Geb.

When using Geb outside the Groovy Shell, it is probably not very convenient to type “browser” over and over again. Thankfully, Geb offers a shortcut: the drive method accepts a closure, where all method-calls are delegated to the underlying Browser instance. In the next example, we extract the items of the sidemenu from the Geb-Homepage. We also show another handy shortcut: instead of find, we can use $. Those already familiar with jQuery, probably aren’t too surprised by this choice.

browser.drive({
    go "http://www.gebish.org/"
    $(".sidemenu a").each({
        element -> println element.text()
    })
})

browser.drive({ go "http://www.gebish.org/" $(".sidemenu a").each({ element -> println element.text() }) })

You have seen, how easy it is to do browser automation with Geb. In simple scripts, it is enough to create a Browser instance and use its drive method. But Geb is much more than a helper for browser automation scripts. So let’s have look at something more interesting: Testing.

Integration with the Spock Testing Framework

Spock is one of the most popular testing frameworks for the Groovy language. And Geb comes with an excellent integration for Spock. I won’t go through the details of configuring Geb and Spock in a maven (or gradle) project. Detailed instructions, how to do this, can be found in the Book of Geb. I can also recommend the geb-maven-example on GitHub.

The Spock integration of Geb comes with a subclass of Spock’s Specification class. Inside test methods, you have the same possibilities as when using the Browser.drive method. You have access to a Browser instance via the browser property. Geb takes care of configuring and starting the browser before the tests (as well as closing the browser once the tests are finished). Just like inside a drive block, you don’t have to use the browser property. All unknown methods are directly delegated to it. This makes the tests very clean and readable. As an example, the following test makes sure, that the Geb homepage is the first result when searching for “Groovy Browser Automation”.

import geb.spock.GebSpec
 
class SearchSpec extends GebReportingSpec {
    def "search 'Groovy Browser Automation' in duckduckgo"() {
        given: "we are on the duckduckgo search-engine"
            go "http://duckduckgo.com"
 
        when: "we search for 'Groovy Browser Automation'"
            $("#search_form_homepage").q = "Groovy Browser Automation"
            $("#search_button_homepage").click()
 
        then: "the first result is the geb website"
            assert $("#links").find(".links_main a", 0).attr("href") == "http://www.gebish.org/"            
    }
}

import geb.spock.GebSpecclass SearchSpec extends GebReportingSpec { def "search 'Groovy Browser Automation' in duckduckgo"() { given: "we are on the duckduckgo search-engine" go "http://duckduckgo.com" when: "we search for 'Groovy Browser Automation'" $("#search_form_homepage").q = "Groovy Browser Automation" $("#search_button_homepage").click() then: "the first result is the geb website" assert $("#links").find(".links_main a", 0).attr("href") == "http://www.gebish.org/" } }

By default, Geb searches the classpath for an available WebDriver. This is enough, if you just want to play around. In a real-world-project however, there are more things to consider. In the CI environment you’d want to run the tests in a headless mode (for example using PhantomJS and GhostDriver). If you’re developing a ROCA application, perhaps you also want to check how your application behaves when JavaScript is disabled. Then HtmlDriver is a possible option. This can be achieved by defining different profiles for Geb. Just place a file GebConfig.groovy inside your classpath and Geb will automatically read profile definitions from there. The profile itself is then determined by the system property geb.build.profile. Take a look at my geb-demo project on GitHub to see profiles in action. The project’s README file also contains detailed instructions, how to execute the tests with maven and how to set up the necessary infrastructure (e.g. a PhantomJS daemon).

Tip: Geb provides yet another base class for spock-tests: GebReportingSpec. The only difference to GebSpec is, that GebReportingSpec automatically takes a screenshot for failed tests. This can be very helpful when you search the reason for failing tests.

The Page Object Pattern

Tests are often treated as second-class citizens. But the same principles, that adhere to production code, can and must be applied to test-code. “Open/Closed” and “DRY” are such principles. And the Page Object Pattern is a means to observe these two. The idea behind page objects is quite simple: Each screen (page) of the web application is represented by an object (page object). Similar pages are represented by objects of the same class. The information and possible user interactions of a page (or a class of pages) are described by the properties and methods of the corresponding page object (or its class).

Geb provides support for the page object pattern via the Page class. The elements and possible actions of a page are described inside the content block using a simple DSL. To illustrate how this works, we take the movie database application of Tobias Flohre’s recent blog post on ROCA and write some acceptance tests. The code of the following examples (including maven configuration and README) can be found in my geb-demo project on GitHub.

One can quickly make out candidates for page objects in the movie database: The initial page could be labeled MovieListPage. When you click on a movie, the details to this movie are displayed (MovieDetailPage). And a click on the “Edit” button leads to a simple form to change these details (MovieEditPage). The implementation for the MovieListPage could look like this:

import geb.Page
 
class MovieListPage extends Page {
    static content = {
        searchform { $(".form-search") }
        searchSubmitButton { searchform.find(type: "submit")}
 
	// this defines a method "searchFor" that takes a single argument
	// the search-form is filled with the given argument and the search-button is clicked
        searchFor { searchTerm ->
            searchform.searchString = searchTerm
            searchSubmitButton.click()
        }
 
	// required=false is needed, because otherwise Geb would automatically throw
        // an AssertionException when the method returns nothing
        movie(required: false) { movieName -> $("td", text: movieName).parent() }
        containsMovie(required: false) { movieName ->  movie(movieName).present }
 
        movieCount { $("#pageContent tr").size() }
    }
}

import geb.Pageclass MovieListPage extends Page { static content = { searchform { $(".form-search") } searchSubmitButton { searchform.find(type: "submit")}// this defines a method "searchFor" that takes a single argument // the search-form is filled with the given argument and the search-button is clicked searchFor { searchTerm -> searchform.searchString = searchTerm searchSubmitButton.click() }// required=false is needed, because otherwise Geb would automatically throw // an AssertionException when the method returns nothing movie(required: false) { movieName -> $("td", text: movieName).parent() } containsMovie(required: false) { movieName -> movie(movieName).present } movieCount { $("#pageContent tr").size() } } }

This definition of the MovieListPage is now the base for tests of the movie database’s search feature. In the following example, we navigate to “http://localhost:8080/moviedatabase”. With the at-method, we tell Geb, that the current screen should be represented by an instance of MovieListPage. After doing so, the Browser automatically delegates all unknown method calls to this object. And because GebSpec delegates all calls to the underlying Browser instance, we can directly call any method of the MovieListPage (e.g. the searchFor method to execute a search). At the end of the test, you can see one of Geb’s other nice features: asynchronous content lookup. Because the search result is loaded via ajax, there is no full page reload. So we tell Geb to wait up to 5 seconds (the default timeout) for the search result to show up. If the movie list doesn’t contain “Star Wars” after 5 seconds, the test fails.

class SearchSpec extends GebSpec {
    def "search for 'star' contains movie 'Star Wars'"() {
        given: "we are on the movie database homepage"
            go "http://localhost:8080/moviedatabase"
            at MovieListPage
 
        when: "we search for 'star'"
            searchFor("star")
            at MovieListPage
 
        then: "the search result contains 'Star Wars'"
            // the waitFor is needed because the movie-database uses javascript
            // heavily and the click on the searchbutton doesnt trigger a page-reload
            waitFor { containsMovie("Star Wars") }
    }
 
    def "search for 'foo' returns empty result"() {
        given: "we are on the movie database homepage":
            go "http://localhost:8080/moviedatabase"
            at MovieListPage
 
        when: "we search for 'star'"
            searchFor("foo")
            at MovieListPage
 
        then: "the search result is empty"
            waitFor { movieCount() == 0 }
    }
}

class SearchSpec extends GebSpec { def "search for 'star' contains movie 'Star Wars'"() { given: "we are on the movie database homepage" go "http://localhost:8080/moviedatabase" at MovieListPage when: "we search for 'star'" searchFor("star") at MovieListPage then: "the search result contains 'Star Wars'" // the waitFor is needed because the movie-database uses javascript // heavily and the click on the searchbutton doesnt trigger a page-reload waitFor { containsMovie("Star Wars") } }def "search for 'foo' returns empty result"() { given: "we are on the movie database homepage": go "http://localhost:8080/moviedatabase" at MovieListPage when: "we search for 'star'" searchFor("foo") at MovieListPagethen: "the search result is empty" waitFor { movieCount() == 0 } } }

Conclusion

Geb is not only a very nice DSL for browser automation, it has also many powerful features. Most notably the jQuery-like selection API and the support for the Page Object Pattern. Another highlight is the asynchronous content lookup. There is support for existing test frameworks like Spock, easyb, JUnit and TestNG. Integration for JBehave or cucumber-jvm is also possible with the BindingUpdater. Getting started is easy and the documentation answers questions before they arise. There is no reason why you shouldn’t give it a try 😉