Geb Browser Automation Testing with Groovy: A Practical Guide

Geb Browser Automation Testing with Groovy: A Practical Guide

Geb is a browser automation library for Groovy that combines WebDriver's power with a jQuery-inspired content DSL and Groovy's concise syntax. It integrates naturally with Spock for behavior-driven browser tests.

What Geb Provides

Geb wraps Selenium WebDriver but adds:

  • A CSS/jQuery-like content DSL for finding elements
  • Page Object support built into the framework
  • Automatic waiting for dynamic content
  • Groovy's readable syntax for assertions

Setup

// build.gradle
dependencies {
    testImplementation 'org.gebish:geb-spock:7.0'
    testImplementation 'org.seleniumhq.selenium:selenium-chrome-driver:4.15.0'
    testImplementation 'io.github.bonigarcia:webdrivermanager:5.6.3'
    testImplementation 'org.spockframework:spock-core:2.3-groovy-4.0'
}

Configure the driver in GebConfig.groovy at the root of src/test/resources:

// 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 = "http://localhost:8080"
waiting {
    timeout = 5
    retryInterval = 0.1
}

Page Objects

Pages define the structure of a URL and its content:

import geb.Page

class LoginPage extends Page {

    static url = "/login"

    static at = { title == "Login — MyApp" }

    static content = {
        emailField { $("input[name='email']") }
        passwordField { $("input[name='password']") }
        submitButton { $("button[type='submit']") }
        errorMessage(required: false) { $(".error-message") }
    }

    void loginAs(String email, String password) {
        emailField.value(email)
        passwordField.value(password)
        submitButton.click()
    }
}

class DashboardPage extends Page {

    static url = "/dashboard"

    static at = { $("h1").text() == "Dashboard" }

    static content = {
        welcomeMessage { $(".welcome-msg") }
        logoutLink { $("a[href='/logout']") }
    }
}

The at closure is the page verification — Geb asserts it when navigating to the page.

Writing Geb Spock Tests

import geb.spock.GebSpec

class LoginSpec extends GebSpec {

    def "successful login redirects to dashboard"() {
        given:
        go "/login"

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

        then:
        at DashboardPage
        $(".welcome-msg").text().contains("Welcome")
    }

    def "invalid credentials show error message"() {
        given:
        to LoginPage

        when:
        page.loginAs("wrong@example.com", "badpassword")

        then:
        at LoginPage
        page.errorMessage.text() == "Invalid email or password"
    }
}

to PageClass navigates and verifies the page via its at check. at PageClass asserts the current page matches without navigating.

Content DSL

Geb's content DSL uses CSS selectors (via jQuery-like $):

static content = {
    // Basic selector
    header { $("h1") }

    // With index
    firstItem { $("ul li", 0) }

    // With attribute
    submitBtn { $("button", type: "submit") }

    // Nested
    nav { $("nav") }
    navLinks { nav.find("a") }

    // Dynamic — re-evaluated each access
    loadingSpinner(cache: false) { $(".spinner") }

    // Optional — no error if absent
    tooltip(required: false) { $(".tooltip") }

    // With wait — waits for content to appear
    notification(wait: true) { $(".notification") }
}

Waiting for Dynamic Content

Geb's waitFor blocks retry until the condition passes or timeout:

def "async operation completes"() {
    when:
    $("button.start-async").click()

    then:
    waitFor { $(".result").text() == "Done" }
}

def "table loads after API call"() {
    when:
    $("button.load-data").click()

    then:
    waitFor(10) { $("table tbody tr").size() > 0 }
}

Content definitions with wait: true use the global waiting config automatically.

Modules for Reusable Components

Modules encapsulate UI components used across multiple pages:

import geb.Module

class NavbarModule extends Module {

    static content = {
        userMenu { $(".user-menu") }
        searchInput { $("input[name='search']") }
    }

    void search(String term) {
        searchInput.value(term)
        searchInput << Keys.ENTER
    }
}

class ProductPage extends Page {

    static url = "/products"

    static content = {
        navbar { module NavbarModule, $("nav") }
        productGrid { $(".product-grid") }
        productCards { $(".product-card") }
    }
}

Form Interaction

def "creating a new product"() {
    given:
    to ProductFormPage

    when:
    page.nameField.value("Widget Pro")
    page.priceField.value("29.99")
    page.categorySelect.selected = "Electronics"
    page.descriptionArea.value("A great widget")
    page.submitButton.click()

    then:
    waitFor { at ProductDetailPage }
    page.productName.text() == "Widget Pro"
}

Geb supports value() for text inputs, selected / selectedText for selects, and keyboard events via Groovy's << operator.

Taking Screenshots

def "report page matches design"() {
    when:
    to ReportPage

    then:
    report "report-page-baseline"  // saves screenshot to build/reports/geb/
}

Configure report directory in GebConfig.groovy:

reportsDir = "build/reports/geb"

CI Configuration

For headless Chrome on CI:

// GebConfig.groovy — environment-aware config
environments {
    ci {
        driver = {
            def options = new ChromeOptions()
            options.addArguments(
                '--headless',
                '--no-sandbox',
                '--disable-dev-shm-usage',
                '--window-size=1920,1080'
            )
            WebDriverManager.chromedriver().setup()
            new ChromeDriver(options)
        }
    }
}

Run with: ./gradlew test -Dgeb.env=ci

When Geb Fits

Geb is the right choice for Groovy/Java projects already using Spock. The combination gives you consistent BDD-style tests from unit level through end-to-end browser tests, all in the same framework and language.

Read more