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.