Geb + Gradle CI Setup: Running Browser Tests in GitHub Actions
Getting Geb tests running locally is one thing. Getting them to run reliably in CI — with headless Chrome, proper reporting, and parallel execution — takes a bit more configuration. This guide covers the full Gradle + GitHub Actions setup.
Gradle Configuration
Build File
A complete build.gradle for Geb with Spock:
plugins {
id 'groovy'
id 'java'
}
group = 'com.example'
version = '1.0.0'
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
repositories {
mavenCentral()
}
ext {
gebVersion = '7.0'
spockVersion = '2.3-groovy-4.0'
seleniumVersion = '4.18.1'
groovyVersion = '4.0.20'
}
dependencies {
implementation "org.apache.groovy:groovy:${groovyVersion}"
testImplementation "org.gebish:geb-spock:${gebVersion}"
testImplementation "org.spockframework:spock-core:${spockVersion}"
testImplementation "org.seleniumhq.selenium:selenium-java:${seleniumVersion}"
testImplementation "io.github.bonigarcia:webdrivermanager:5.8.0"
testImplementation "org.slf4j:slf4j-simple:2.0.12"
}
test {
useJUnitPlatform()
// Parallel execution
maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1
// Pass system properties to tests
systemProperty 'geb.env', System.getProperty('geb.env', 'ci')
systemProperty 'geb.build.reportsDir', reporting.file('geb')
// JVM args for Chrome on Linux
jvmArgs '--add-opens=java.base/java.lang=ALL-UNNAMED'
reports {
html.required = true
junitXml.required = true
}
}GebConfig.groovy with Environments
src/test/resources/GebConfig.groovy:
import io.github.bonigarcia.wdm.WebDriverManager
import org.openqa.selenium.chrome.ChromeDriver
import org.openqa.selenium.chrome.ChromeOptions
import org.openqa.selenium.firefox.FirefoxDriver
import org.openqa.selenium.firefox.FirefoxOptions
// Default base URL — override per environment
baseUrl = System.getProperty('baseUrl', 'https://staging.example.com')
// Reports directory
reportsDir = 'build/reports/geb'
// Wait configuration
waiting {
timeout = 15
retryInterval = 0.5
includeCauseInMessage = true
}
// Environment-specific driver config
environments {
// Local development (default)
local {
driver = {
WebDriverManager.chromedriver().setup()
new ChromeDriver()
}
}
// CI environment — headless Chrome
ci {
driver = {
WebDriverManager.chromedriver().setup()
def options = new ChromeOptions()
options.addArguments(
'--headless=new',
'--no-sandbox',
'--disable-dev-shm-usage',
'--disable-gpu',
'--window-size=1920,1080',
'--disable-extensions',
'--disable-infobars'
)
new ChromeDriver(options)
}
}
// Firefox for cross-browser
firefox {
driver = {
WebDriverManager.firefoxdriver().setup()
def options = new FirefoxOptions()
options.addArguments('--headless')
new FirefoxDriver(options)
}
}
}Run with a specific environment:
./gradlew test -Dgeb.env=ci -DbaseUrl=https://staging.example.comGitHub Actions Workflow
.github/workflows/browser-tests.yml:
name: Browser Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
schedule:
# Run nightly against staging
- cron: '0 2 * * *'
jobs:
browser-tests:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: gradle
- name: Install Chrome
uses: browser-actions/setup-chrome@v1
with:
chrome-version: stable
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Run Geb tests
run: ./gradlew test -Dgeb.env=ci -DbaseUrl=${{ vars.STAGING_URL }}
env:
DISPLAY: ':99'
- name: Upload test reports
uses: actions/upload-artifact@v4
if: always()
with:
name: geb-reports
path: |
build/reports/geb/
build/reports/tests/
retention-days: 14
- name: Publish test results
uses: EnricoMi/publish-unit-test-result-action@v2
if: always()
with:
files: build/test-results/**/*.xmlParallel Test Execution
Gradle Parallel Forks
The maxParallelForks setting in build.gradle controls how many test JVMs run simultaneously. Each fork gets its own browser instance:
test {
maxParallelForks = 4 // 4 browsers in parallel
}For CI, set this to available CPU cores:
maxParallelForks = Runtime.runtime.availableProcessors()Test Isolation
For parallel tests to work reliably, each test must be independent:
class OrderSpec extends GebSpec {
def setup() {
// Each test starts fresh — don't share state
to LoginPage
loginAs('testuser@example.com', 'password')
}
def cleanup() {
// Clean up test data after each test
deleteTestOrders()
}
def "create new order"() {
given:
to OrdersPage
when:
newOrderButton.click()
// ...
then:
// ...
}
}Gradle Task for Different Test Suites
Separate fast smoke tests from the full suite:
task smokeTest(type: Test) {
description = 'Runs smoke tests against production'
group = 'verification'
useJUnitPlatform()
include '**/Smoke*Spec.class'
systemProperty 'geb.env', 'ci'
systemProperty 'baseUrl', System.getProperty('baseUrl', 'https://example.com')
}
task e2eTest(type: Test) {
description = 'Runs full E2E suite against staging'
group = 'verification'
useJUnitPlatform()
exclude '**/Smoke*Spec.class'
maxParallelForks = 4
systemProperty 'geb.env', 'ci'
systemProperty 'baseUrl', System.getProperty('baseUrl', 'https://staging.example.com')
}Run specific suites:
./gradlew smokeTest # smoke only
./gradlew e2eTest -DbaseUrl=https://staging.example.com # full suiteScreenshot on Failure
Geb captures screenshots automatically for failed tests. Configure the report directory:
// GebConfig.groovy
reportsDir = new File(System.getProperty('geb.build.reportsDir', 'build/reports/geb'))// build.gradle
test {
systemProperty 'geb.build.reportsDir', reporting.file('geb').absolutePath
}Screenshots are saved as <TestName>-<timestamp>-failure.png in the reports directory. The GitHub Actions artifact upload step captures them for failed runs.
Caching Dependencies
Speed up CI by caching the Gradle wrapper and dependencies:
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: gradle # caches ~/.gradleWebDriverManager also caches downloaded drivers. Cache it too:
- name: Cache WebDriver binaries
uses: actions/cache@v4
with:
path: ~/.cache/selenium
key: ${{ runner.os }}-webdriver-${{ hashFiles('build.gradle') }}Troubleshooting Common CI Issues
DevToolsActivePort file doesn't exist: Chrome failed to start. Add --no-sandbox and --disable-dev-shm-usage to ChromeOptions. These are required in containerized environments.
Tests pass locally, fail in CI: Check timing. CI machines are slower. Increase the waiting.timeout in GebConfig.groovy for the ci environment.
OutOfMemoryError in parallel runs: Each fork is a separate JVM. Reduce maxParallelForks or increase heap with jvmArgs '-Xmx512m' per fork.
Flaky tests on GitHub Actions: The free tier uses shared runners with variable performance. Use waitFor instead of Thread.sleep, and don't rely on animation timing.
Summary
A well-configured Geb + Gradle + GitHub Actions pipeline gives you:
- Headless Chrome running on every push and PR
- Parallel execution to keep suite runtime manageable
- Screenshots and HTML reports uploaded as artifacts for every failed run
- Separate smoke vs. full suite tasks for different triggers
The investment in setup pays off quickly — reliable CI feedback on browser tests catches regressions before they reach production.