Geb + Gradle CI Setup: Running Browser Tests in GitHub Actions

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.com

GitHub 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/**/*.xml

Parallel 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 suite

Screenshot 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 ~/.gradle

WebDriverManager 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.

Read more

Start now free