Geb vs Selenium: Which Browser Automation Tool Should You Choose?

Geb vs Selenium: Which Browser Automation Tool Should You Choose?

Geb is built on Selenium WebDriver. So comparing them isn't quite an apples-to-apples fight — it's more about whether the abstraction layer Geb adds is worth the dependency. This guide covers the real differences in day-to-day usage.

What Geb Adds on Top of Selenium

Geb wraps Selenium's WebDriver API with:

  1. Content DSL: Named selectors via $() with jQuery syntax, defined in Page classes
  2. Page Object model: First-class Page and Module classes baked into the framework
  3. Built-in waitFor: Polling mechanism — no manual WebDriverWait setup
  4. Spock integration: GebSpec base class for Spock tests
  5. Auto-reporting: Screenshots and HTML reports on failure, no custom listener required
  6. at checkers: Declarative page verification built into navigation

Syntax Comparison

Finding Elements

Selenium (Java):

WebElement button = driver.findElement(By.cssSelector("button[type=submit]"));
List<WebElement> items = driver.findElements(By.cssSelector("ul.results li"));
WebElement byText = driver.findElement(By.xpath("//button[text()='Save']"));

Geb:

$('button[type=submit]')
$('ul.results li')
$('button', text: 'Save')

Geb's $() function handles CSS selectors, attribute matching, and text matching in a single consistent API.

Interacting with Elements

Selenium:

driver.findElement(By.name("email")).clear();
driver.findElement(By.name("email")).sendKeys("user@example.com");
driver.findElement(By.cssSelector("button.submit")).click();
String value = driver.findElement(By.id("status")).getText();

Geb:

$('input[name=email]').value('user@example.com')  // clears and sets
$('button.submit').click()
$('#status').text()

Waiting for Elements

Selenium (requires explicit setup):

WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector("div.results")));
wait.until(ExpectedConditions.textToBe(By.cssSelector("span.status"), "Complete"));

Geb:

waitFor { $('div.results').displayed }
waitFor { $('span.status').text() == 'Complete' }

waitFor evaluates any Groovy closure, not just pre-built ExpectedConditions.

Page Object Model

Selenium Page Object (Java, without PageFactory)

public class LoginPage {
    private WebDriver driver;
    private By emailField = By.name("email");
    private By passwordField = By.name("password");
    private By submitButton = By.cssSelector("button[type=submit]");
    
    public LoginPage(WebDriver driver) {
        this.driver = driver;
    }
    
    public void loginAs(String email, String password) {
        driver.findElement(emailField).clear();
        driver.findElement(emailField).sendKeys(email);
        driver.findElement(passwordField).clear();
        driver.findElement(passwordField).sendKeys(password);
        driver.findElement(submitButton).click();
    }
}

Geb Page Object

class LoginPage extends Page {
    static url = '/login'
    static at = { title == 'Login' }

    static content = {
        emailField { $('input[name=email]') }
        passwordField { $('input[name=password]') }
        submitButton { $('button[type=submit]') }
    }

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

The Geb version is shorter and has the at checker built in. The static content DSL also supports wait: true, required: false, and caching without extra code.

Selenium:

driver.get("https://example.com/login");
// No built-in verification that you're on the right page

Geb:

to LoginPage    // navigates to LoginPage.url AND verifies LoginPage.at
at DashboardPage  // verifies without navigating

to and at give you explicit, readable page verification.

Test Structure

Selenium + JUnit (Java)

@ExtendWith(SeleniumExtension.class)
public class LoginTest {
    private WebDriver driver;

    @BeforeEach
    public void setUp() {
        WebDriverManager.chromedriver().setup();
        ChromeOptions options = new ChromeOptions();
        options.addArguments("--headless");
        driver = new ChromeDriver(options);
    }

    @AfterEach
    public void tearDown() {
        if (driver != null) driver.quit();
    }

    @Test
    public void userCanLogin() {
        driver.get("https://example.com/login");
        driver.findElement(By.name("email")).sendKeys("user@test.com");
        driver.findElement(By.name("password")).sendKeys("password");
        driver.findElement(By.cssSelector("button[type=submit]")).click();

        WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
        wait.until(ExpectedConditions.urlContains("/dashboard"));

        assertEquals("Dashboard", driver.getTitle());
    }
}

Geb + Spock

class LoginSpec extends GebSpec {
    def "user can log in"() {
        given:
        to LoginPage

        when:
        loginAs('user@test.com', 'password')

        then:
        at DashboardPage
    }
}

Driver setup, teardown, and reporting are handled by GebSpec. No boilerplate.

Modules vs Custom Components

Selenium: No built-in component model. Teams typically create helper classes that take a WebElement parameter:

public class NavBarComponent {
    private final WebElement root;
    public NavBarComponent(WebElement root) { this.root = root; }
    public void clickLogo() { root.findElement(By.cssSelector("a.logo")).click(); }
}

Geb: First-class Module support:

class NavBarModule extends Module {
    static content = {
        logo { $('a.logo') }
        searchInput { $('input[name=search]') }
    }
}

class HomePage extends Page {
    static content = {
        navbar { module NavBarModule }
    }
}

Modules compose cleanly and use the same content DSL as pages.

Reporting

Selenium: No built-in reporting. You need to add a JUnit listener or integrate a third-party library (Allure, Extent Reports) to get screenshots on failure.

Geb: Automatic HTML reports and screenshots on test failure. Configure the directory:

reportsDir = 'build/reports/geb'

Every failed test gets a screenshot and page source saved automatically.

When to Choose Selenium Directly

  • Java-only team: Geb requires Groovy. If your team won't adopt Groovy, use Selenium with Java and a framework like JUnit + PicoContainer.
  • Non-JVM stack: Selenium has official bindings for Python, JavaScript, Ruby, C#, and Kotlin. Geb is JVM-only.
  • Selenium Grid / cloud providers: Geb works with Selenium Grid and cloud providers (BrowserStack, Sauce Labs), but the setup is the same as raw Selenium — you're configuring the driver in GebConfig.
  • Already using Playwright/Cypress: If you're starting fresh and not on the JVM, Playwright (Node.js, Python) or Cypress are worth evaluating. Geb's niche is Groovy/Java shops that want a higher-level API over WebDriver.

When to Choose Geb

  • Groovy/Spock shop: If you're already writing Spock tests, Geb is the natural extension to the browser.
  • Existing Selenium tests to migrate: Geb sits on top of Selenium; you can migrate incrementally.
  • JVM team wanting less boilerplate: Geb eliminates driver setup/teardown, explicit waits setup, and Page Object boilerplate.
  • Need built-in reporting: Geb's auto-screenshots on failure are genuinely useful without any extra configuration.

Summary

Feature Selenium (Java) Geb (Groovy)
Element selectors By.cssSelector(), By.xpath() $('css'), $('tag', attr: val)
Waiting Manual WebDriverWait waitFor {} closure
Page Objects DIY convention Built-in Page class with content DSL
Component model DIY helper classes Built-in Module class
At checking Manual assertions at PageClass + static at = {}
Reporting Requires plugin Built-in, screenshots on failure
Language Java (+ Kotlin) Groovy (JVM)
Learning curve Low (Java dev) Low (if you know Groovy/Spock)

Geb doesn't replace Selenium — it wraps it. The question is whether Groovy's conciseness and Geb's built-in patterns are worth the added dependency for your team. For Groovy/Spock shops, the answer is usually yes.

Read more

Start now free