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:
- Content DSL: Named selectors via
$()with jQuery syntax, defined in Page classes - Page Object model: First-class
PageandModuleclasses baked into the framework - Built-in
waitFor: Polling mechanism — no manualWebDriverWaitsetup - Spock integration:
GebSpecbase class for Spock tests - Auto-reporting: Screenshots and HTML reports on failure, no custom listener required
atcheckers: 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.
Navigation
Selenium:
driver.get("https://example.com/login");
// No built-in verification that you're on the right pageGeb:
to LoginPage // navigates to LoginPage.url AND verifies LoginPage.at
at DashboardPage // verifies without navigatingto 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
driverin 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.