Geb Browser Automation with Spock: Getting Started Guide

Geb Browser Automation with Spock: Getting Started Guide

Geb is a Groovy-based browser automation library built on top of WebDriver. It combines the power of Selenium's WebDriver with Groovy's expressive syntax and the jQuery-style content DSL. When paired with Spock, the result is browser tests that read almost like plain English.

This guide covers Geb fundamentals — setup, core concepts, and writing your first Spock-based browser tests.

What Makes Geb Different

Most browser automation tools are direct WebDriver wrappers. Geb adds several layers that make tests more maintainable:

  • Content DSL: Define page elements with a jQuery-inspired selector syntax, named and reusable
  • Page Object support: First-class Page and Module classes, not bolted on as a pattern
  • Spock integration: Native support for Spock's given/when/then blocks and data-driven tests
  • Implicit waiting: Built-in waitFor {} that polls until a condition is true, no Thread.sleep

Geb is built for Groovy, but it runs on the JVM — your existing Java/Groovy build infrastructure works without changes.

Project Setup

Add Geb and Spock to your build.gradle:

plugins {
    id 'groovy'
}

repositories {
    mavenCentral()
}

dependencies {
    testImplementation 'org.gebish:geb-spock:7.0'
    testImplementation 'org.spockframework:spock-core:2.3-groovy-4.0'
    testImplementation 'org.seleniumhq.selenium:selenium-java:4.18.1'
    testImplementation 'io.github.bonigarcia:webdrivermanager:5.8.0'
}

test {
    useJUnitPlatform()
}

Create src/test/resources/GebConfig.groovy:

import io.github.bonigarcia.wdm.WebDriverManager
import org.openqa.selenium.chrome.ChromeDriver
import org.openqa.selenium.chrome.ChromeOptions

driver = {
    WebDriverManager.chromedriver().setup()
    def options = new ChromeOptions()
    options.addArguments('--headless', '--no-sandbox', '--disable-dev-shm-usage')
    new ChromeDriver(options)
}

baseUrl = 'https://example.com'

waiting {
    timeout = 10
    retryInterval = 0.5
}

Core Concepts

The Browser Object

Geb's entry point is the Browser class. In Spock tests, you extend GebSpec and the browser is managed automatically:

import geb.spock.GebSpec

class LoginSpec extends GebSpec {
    def "user can log in"() {
        given: "the user is on the login page"
        go '/login'

        when: "they enter credentials"
        $('input[name=email]').value('user@example.com')
        $('input[name=password]').value('secret')
        $('button[type=submit]').click()

        then: "they are redirected to the dashboard"
        waitFor { currentUrl.contains('/dashboard') }
    }
}

The $() Function

The $() function is Geb's content selector, modeled after jQuery:

// By CSS selector
$('input[type=text]')

// By index (zero-based)
$('li', 0)          // first <li>
$('li', 1)          // second <li>

// By attribute
$('input', name: 'username')

// Combined
$('div.results li', 0)

The object returned by $() is a Navigator. Common operations:

def input = $('input[name=search]')

input.value('Geb testing')      // set value
input.value()                   // get value
input.click()                   // click
input.text()                    // get text content
input.getAttribute('class')     // get HTML attribute
input.displayed                 // is visible?
input.present                   // exists in DOM?

Writing Spock Tests with Geb

Spock's given/when/then blocks pair naturally with browser interactions:

class SearchSpec extends GebSpec {

    def "search results appear for valid query"() {
        given: "user is on the homepage"
        go '/'

        when: "they search for a term"
        $('input[name=q]').value('automated testing')
        $('button[type=submit]').click()

        then: "results are displayed"
        waitFor { $('div.results').present }
        $('div.results li').size() > 0
    }

    def "empty search shows no results message"() {
        given:
        go '/search'

        when:
        $('button[type=submit]').click()

        then:
        waitFor { $('p.no-results').displayed }
        $('p.no-results').text() == 'No results found'
    }
}

Data-Driven Tests

Spock's where: block works seamlessly with Geb for testing multiple inputs:

class FormValidationSpec extends GebSpec {

    def "form validates required fields"() {
        given:
        go '/register'

        when: "user submits with #field empty"
        fillForm(email: email, name: name, password: password)
        $('button[type=submit]').click()

        then: "validation error appears for #field"
        waitFor { $("span.error[data-field=${field}]").displayed }

        where:
        field      | email             | name   | password
        'email'    | ''                | 'Alice'| 'pass123'
        'name'     | 'alice@test.com'  | ''     | 'pass123'
        'password' | 'alice@test.com'  | 'Alice'| ''
    }

    private void fillForm(Map data) {
        if (data.email) $('input[name=email]').value(data.email)
        if (data.name) $('input[name=name]').value(data.name)
        if (data.password) $('input[name=password]').value(data.password)
    }
}

waitFor: Handling Async Content

Geb's waitFor {} block polls until the closure returns truthy, up to the configured timeout:

// Wait for element to appear
waitFor { $('div.modal').displayed }

// Wait for text to change
waitFor { $('span.status').text() == 'Saved' }

// Custom timeout
waitFor(20) { $('div.results li').size() > 0 }

// Custom timeout + message
waitFor(10, 'Results did not load in time') {
    $('div.results').present
}

Do not use Thread.sleep — waitFor is more reliable and gives better error messages when it times out.

Running the Tests

./gradlew test

Geb generates HTML reports in build/reports/geb/. Failed tests include screenshots automatically.

To run headfully (show the browser):

// In GebConfig.groovy
driver = {
    WebDriverManager.chromedriver().setup()
    new ChromeDriver()  // no headless option
}

Common Pitfalls

StaleElementReferenceException: Geb lazily evaluates $() calls, which reduces staleness. If you store a navigator in a variable and the DOM updates, re-fetch it.

Missing waitFor: If an assertion fires before an async operation completes, add waitFor. Geb won't auto-wait on arbitrary closures outside the waiting config.

GebConfig not found: The file must be at src/test/resources/GebConfig.groovy (not src/main). Groovy's classpath lookup finds it there.

What's Next

The basics covered here — $(), waitFor, and GebSpec — handle most cases. The real power comes from organizing tests into Page Objects, which keeps selectors in one place and makes tests resilient to UI changes. That's covered in the page objects guide.

For teams already using Spock for unit tests, Geb slots in naturally. The same assertion style, the same data-driven patterns, the same reporting — now extended to the browser.

Read more

Start now free