← Back to DevBytes

Gauge: Complete Testing Guide for Developers

What is Gauge?

Gauge is an open-source test automation framework created by ThoughtWorks that takes a fundamentally different approach to test creation. Instead of forcing testers to write tests entirely in code, Gauge separates the test specification (written in Markdown) from the test implementation (written in a programming language of your choice). This separation allows business-readable specifications to coexist with robust, maintainable code.

The core philosophy behind Gauge is simple: tests should be readable by anyone on the team, not just developers. Product owners, business analysts, and QA engineers can read, understand, and even author test specifications in plain Markdown. Meanwhile, developers implement the underlying automation logic in languages like Java, C#, Ruby, Python, JavaScript, or TypeScript.

Gauge is not a Behavior-Driven Development (BDD) framework like Cucumber, though it shares some conceptual similarities. The key difference is that Gauge does not enforce the Given-When-Then syntax. You have complete freedom to structure your specifications however makes sense for your domain — using headings, bullet points, tables, and any Markdown formatting you prefer.

Key Features at a Glance

Why Gauge Matters

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Traditional test automation often suffers from a communication gap. Automated tests written purely in code become opaque to non-technical stakeholders. Meanwhile, BDD frameworks like Cucumber impose rigid syntax rules that can feel unnatural. Gauge bridges this gap elegantly by letting you write specifications in the most natural format possible — structured Markdown — while keeping the implementation in full-fledged programming languages.

Here are the concrete problems Gauge solves:

Getting Started: Installation and Setup

Gauge provides a straightforward CLI-based installation. You'll need the Gauge binary itself, a language plugin for your chosen programming language, and optionally an IDE plugin.

Step 1: Install the Gauge CLI

On macOS with Homebrew:

brew install gauge

On Linux (using the install script):

curl -SsL https://raw.githubusercontent.com/getgauge/gauge/master/install.sh | bash

On Windows, download the installer from the Gauge website or use Chocolatey:

choco install gauge

Verify the installation:

gauge --version

Step 2: Install a Language Plugin

Choose the plugin for your preferred language. For Java:

gauge install java

For Python:

gauge install python

For JavaScript/TypeScript:

gauge install js

For C# (.NET):

gauge install dotnet

For Ruby:

gauge install ruby

Step 3: Create a New Gauge Project

Gauge provides an interactive project scaffolding command:

gauge init java

This prompts you for a project name and creates the complete directory structure with sample files. You can also scaffold non-interactively:

gauge init java my-test-project

Step 4: Install IDE Support (VS Code)

The Gauge VS Code extension provides syntax highlighting, auto-completion for step references, and the ability to run specs directly from the editor. Install it from the VS Code marketplace by searching for "Gauge".

Gauge Project Structure

After running gauge init, you'll see a project structure like this:

my-test-project/
├── specs/
│   └── example.spec
├── src/
│   └── tests/
│       └── StepImplementations.java
├── manifest.json
├── env/
│   └── default.properties
└── .gauge/
    └── (internal config files)

Here's what each part does:

manifest.json Explained

Here's a typical manifest.json for a Java Gauge project:

{
  "language": "java",
  "plugins": [
    "html-report",
    "java"
  ],
  "env": {
    "default": "env/default.properties"
  }
}

The language field tells Gauge which language plugin to use. The plugins array lists active plugins (like the HTML report generator). The env object maps environment names to property files.

Writing Your First Gauge Test

Let's build a complete example: testing a simple login feature for a web application. We'll write the specification in Markdown and implement the steps in Java.

Step 1: Write the Specification File

Create specs/login.spec:

# Login Feature

This specification covers user login scenarios for the application.

## Successful Login with Valid Credentials

Tags: login, smoke, positive

* Open the login page
* Enter username "john.doe@example.com"
* Enter password "SecurePass123!"
* Click the login button
* Verify the dashboard page is displayed
* Verify the welcome message contains "Welcome, John"

## Login Failure with Invalid Password

Tags: login, regression, negative

* Open the login page
* Enter username "john.doe@example.com"
* Enter password "WrongPassword"
* Click the login button
* Verify an error message is displayed
* Verify the error message text is "Invalid username or password"

## Login with Empty Fields

Tags: login, edge-case

| username | password | expectedError              |
|----------|----------|----------------------------|
|          | anypass  | Username is required       |
| user123  |          | Password is required       |
|          |          | Username is required       |

* Open the login page
* Enter username <username>
* Enter password <password>
* Click the login button
* Verify an error message is displayed
* Verify the error message text is <expectedError>

Notice several important things here:

Step 2: Implement the Steps

Now create src/tests/LoginSteps.java:

package tests;

import com.thoughtworks.gauge.Step;
import com.thoughtworks.gauge.Table;
import com.thoughtworks.gauge.TableRow;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

import java.time.Duration;

import static org.junit.Assert.*;

public class LoginSteps {

    private WebDriver driver;
    private WebDriverWait wait;

    public LoginSteps() {
        driver = new ChromeDriver();
        wait = new WebDriverWait(driver, Duration.ofSeconds(10));
    }

    @Step("Open the login page")
    public void openLoginPage() {
        driver.get("https://example-app.com/login");
        driver.manage().window().maximize();
    }

    @Step("Enter username ")
    public void enterUsername(String username) {
        WebElement usernameField = driver.findElement(By.id("username"));
        usernameField.clear();
        usernameField.sendKeys(username);
    }

    @Step("Enter password ")
    public void enterPassword(String password) {
        WebElement passwordField = driver.findElement(By.id("password"));
        passwordField.clear();
        passwordField.sendKeys(password);
    }

    @Step("Click the login button")
    public void clickLoginButton() {
        WebElement loginButton = driver.findElement(By.id("login-btn"));
        loginButton.click();
    }

    @Step("Verify the dashboard page is displayed")
    public void verifyDashboardDisplayed() {
        wait.until(ExpectedConditions.urlContains("/dashboard"));
        String currentUrl = driver.getCurrentUrl();
        assertTrue("Expected URL to contain '/dashboard', but got: " + currentUrl,
                   currentUrl.contains("/dashboard"));
    }

    @Step("Verify the welcome message contains ")
    public void verifyWelcomeMessage(String expectedMessage) {
        WebElement welcomeElement = driver.findElement(By.id("welcome-msg"));
        wait.until(ExpectedConditions.visibilityOf(welcomeElement));
        String actualMessage = welcomeElement.getText();
        assertTrue("Expected welcome message to contain '" + expectedMessage +
                   "', but got: '" + actualMessage + "'",
                   actualMessage.contains(expectedMessage));
    }

    @Step("Verify an error message is displayed")
    public void verifyErrorMessageDisplayed() {
        WebElement errorElement = driver.findElement(By.className("error-message"));
        wait.until(ExpectedConditions.visibilityOf(errorElement));
        assertTrue("Error message should be visible",
                   errorElement.isDisplayed());
    }

    @Step("Verify the error message text is ")
    public void verifyErrorMessageText(String expectedError) {
        WebElement errorElement = driver.findElement(By.className("error-message"));
        String actualError = errorElement.getText();
        assertEquals("Expected error message '" + expectedError +
                     "', but got: '" + actualError + "'",
                     expectedError, actualError);
    }

    // Cleanup after each scenario
    @Step("Close browser")
    public void closeBrowser() {
        if (driver != null) {
            driver.quit();
        }
    }
}

Key implementation details:

Step 3: Run the Tests

Execute all specs in the project:

gauge run specs/

Run a specific spec file:

gauge run specs/login.spec

Run only tests with specific tags:

gauge run --tags "smoke" specs/

Run in parallel (4 concurrent streams):

gauge run --parallel -n 4 specs/

Run with a different environment configuration:

gauge run --env staging specs/

Step Implementation in Different Languages

Gauge's multi-language support means you can implement steps in whatever language your team prefers. Here are the same login steps implemented in Python and JavaScript to illustrate the pattern.

Python Implementation

# src/tests/login_steps.py
from gauge.step import step
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import unittest

driver = None
wait = None

@step("Open the login page")
def open_login_page():
    global driver, wait
    driver = webdriver.Chrome()
    wait = WebDriverWait(driver, 10)
    driver.get("https://example-app.com/login")
    driver.maximize_window()

@step("Enter username ")
def enter_username(username):
    field = driver.find_element(By.ID, "username")
    field.clear()
    field.send_keys(username)

@step("Enter password ")
def enter_password(password):
    field = driver.find_element(By.ID, "password")
    field.clear()
    field.send_keys(password)

@step("Click the login button")
def click_login():
    driver.find_element(By.ID, "login-btn").click()

@step("Verify the dashboard page is displayed")
def verify_dashboard():
    wait.until(EC.url_contains("/dashboard"))
    assert "/dashboard" in driver.current_url

@step("Verify the welcome message contains ")
def verify_welcome_message(message):
    element = driver.find_element(By.ID, "welcome-msg")
    wait.until(EC.visibility_of(element))
    assert message in element.text

@step("Verify an error message is displayed")
def verify_error_displayed():
    element = driver.find_element(By.CLASS_NAME, "error-message")
    wait.until(EC.visibility_of(element))
    assert element.is_displayed()

@step("Verify the error message text is ")
def verify_error_text(expected_error):
    element = driver.find_element(By.CLASS_NAME, "error-message")
    assert expected_error == element.text

JavaScript/TypeScript Implementation

// src/tests/login_steps.ts
import { Step } from 'gauge-ts';
import { Builder, By, until, WebDriver } from 'selenium-webdriver';
import * as assert from 'assert';

let driver: WebDriver;

export class LoginSteps {

    @Step("Open the login page")
    async openLoginPage(): Promise {
        driver = await new Builder().forBrowser('chrome').build();
        await driver.get("https://example-app.com/login");
        await driver.manage().window().maximize();
    }

    @Step("Enter username ")
    async enterUsername(username: string): Promise {
        const field = await driver.findElement(By.id("username"));
        await field.clear();
        await field.sendKeys(username);
    }

    @Step("Enter password ")
    async enterPassword(password: string): Promise {
        const field = await driver.findElement(By.id("password"));
        await field.clear();
        await field.sendKeys(password);
    }

    @Step("Click the login button")
    async clickLogin(): Promise {
        await driver.findElement(By.id("login-btn")).click();
    }

    @Step("Verify the dashboard page is displayed")
    async verifyDashboard(): Promise {
        await driver.wait(until.urlContains("/dashboard"), 10000);
        const url = await driver.getCurrentUrl();
        assert.ok(url.includes("/dashboard"), "URL should contain /dashboard");
    }

    @Step("Verify the welcome message contains ")
    async verifyWelcomeMessage(message: string): Promise {
        const element = await driver.findElement(By.id("welcome-msg"));
        await driver.wait(until.elementIsVisible(element), 10000);
        const text = await element.getText();
        assert.ok(text.includes(message),
                  `Expected message to contain "${message}" but got "${text}"`);
    }

    @Step("Verify an error message is displayed")
    async verifyErrorDisplayed(): Promise {
        const element = await driver.findElement(By.className("error-message"));
        await driver.wait(until.elementIsVisible(element), 10000);
        assert.ok(await element.isDisplayed());
    }

    @Step("Verify the error message text is ")
    async verifyErrorText(expectedError: string): Promise {
        const element = await driver.findElement(By.className("error-message"));
        const text = await element.getText();
        assert.strictEqual(text, expectedError);
    }
}

Advanced Gauge Features

Data-Driven Testing with Tables

Gauge excels at data-driven testing. Tables in spec files can drive multiple iterations of the same scenario. Each row becomes an independent test execution:

# User Registration Validation

## Validate registration with various inputs

Tags: registration, data-driven

| username     | email              | password     | expectedResult |
|--------------|--------------------|--------------|----------------|
| validUser    | user@test.com      | Strong1Pass  | success        |
| short        | short@test.com     | Short1       | error          |
| no-at-sign   | invalidemail       | Strong1Pass  | error          |
| spacesInName | spaces@test.com    | Strong1Pass  | success        |

* Open the registration page
* Enter username <username>
* Enter email <email>
* Enter password <password>
* Submit the registration form
* Verify the result is <expectedResult>

Gauge executes this scenario four times — once for each table row. Each execution is independent, and failures in one row don't block others. The HTML report shows results for each row separately.

Environment Configuration

Create env/default.properties:

# Default environment configuration
base_url = https://example-app.com
browser = chrome
timeout = 10
admin_username = admin
admin_password = AdminSecret123

Create env/staging.properties:

# Staging environment overrides
base_url = https://staging.example-app.com
browser = firefox
timeout = 15
admin_username = staging_admin
admin_password = StagingSecret456

Access these properties in your step implementations using Gauge's environment API. In Java:

import com.thoughtworks.gauge.Gauge;
import com.thoughtworks.gauge.Step;

public class ConfigurableSteps {

    @Step("Open the application")
    public void openApplication() {
        String baseUrl = Gauge.readEnvironmentProperty("base_url");
        String browser = Gauge.readEnvironmentProperty("browser");
        // Use these values to configure WebDriver
        driver = WebDriverFactory.create(browser);
        driver.get(baseUrl);
    }
}

Switch environments at runtime:

gauge run --env staging specs/

Scenario Hooks (Before/After)

Gauge supports setup and teardown hooks at multiple levels. Implement them using special annotations:

import com.thoughtworks.gauge.BeforeScenario;
import com.thoughtworks.gauge.AfterScenario;
import com.thoughtworks.gauge.BeforeSpec;
import com.thoughtworks.gauge.AfterSpec;
import com.thoughtworks.gauge.BeforeSuite;
import com.thoughtworks.gauge.AfterSuite;

public class Hooks {

    @BeforeSuite
    public void beforeSuite() {
        // Runs once before all specs in the project
        System.out.println("=== Starting test suite ===");
    }

    @AfterSuite
    public void afterSuite() {
        // Runs once after all specs complete
        System.out.println("=== Test suite finished ===");
    }

    @BeforeSpec
    public void beforeSpec() {
        // Runs before each .spec file
        System.out.println("Starting spec: " + Gauge.getCurrentSpecification());
    }

    @AfterSpec
    public void afterSpec() {
        // Runs after each .spec file completes
        System.out.println("Finished spec");
    }

    @BeforeScenario
    public void beforeScenario() {
        // Runs before each scenario (heading + its steps)
        // Initialize resources, set up test data
        System.out.println("Setting up scenario...");
    }

    @AfterScenario
    public void afterScenario() {
        // Cleanup after each scenario
        // Close browser, delete test data, reset state
        if (driver != null) {
            driver.quit();
        }
        System.out.println("Scenario cleanup complete");
    }
}

The hook hierarchy follows this order: BeforeSuiteBeforeSpecBeforeScenario → (steps execute) → AfterScenarioAfterSpecAfterSuite.

Custom Messages and Screenshots

You can embed diagnostic information — screenshots, custom messages — directly into Gauge reports:

import com.thoughtworks.gauge.Step;
import com.thoughtworks.gauge.Gauge;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;

public class ReportingSteps {

    @Step("Take a screenshot for debugging")
    public void captureScreenshot() {
        // Capture screenshot as base64 and embed in report
        TakesScreenshot ts = (TakesScreenshot) driver;
        String screenshot = ts.getScreenshotAs(OutputType.BASE64);
        Gauge.writeMessage("Screenshot captured at critical point");
        Gauge.captureScreenshot(screenshot);
    }

    @Step("Log custom diagnostic info")
    public void logDiagnostics() {
        Gauge.writeMessage("Current URL: " + driver.getCurrentUrl());
        Gauge.writeMessage("Page title: " + driver.getTitle());
        Gauge.writeMessage("Available cookies: " + driver.manage().getCookies());
    }
}

Step Recovery and Retry

Gauge supports step-level retry logic for flaky operations:

import com.thoughtworks.gauge.Step;
import com.thoughtworks.gauge.ContinueOnFailure;

public class ResilientSteps {

    @ContinueOnFailure
    @Step("Attempt a flaky operation")
    public void flakyOperation() {
        // If this step fails, Gauge continues to the next step
        // rather than aborting the entire scenario
        try {
            driver.findElement(By.id("dynamic-element")).click();
        } catch (Exception e) {
            Gauge.writeMessage("Flaky operation failed, continuing anyway");
        }
    }
}

For true retry behavior, combine with hooks or implement custom retry logic in your step methods.

Refactoring and Step Renaming

When you change step text in a spec file, Gauge helps you keep implementations in sync. Run the validation command:

gauge validate specs/

This reports any steps in spec files that lack corresponding implementations, and any orphaned implementations that no longer match any spec step. It's essentially a compilation check for your test suite.

Best Practices for Gauge Projects

1. Structure Specs by Business Capability, Not by Page

Organize your spec files around business features, not around UI pages or API endpoints. A spec file should tell a complete business story:

# Good: Business-oriented structure
specs/
├── user-registration.spec
├── product-search.spec
├── checkout-flow.spec
└── order-tracking.spec

# Avoid: Page-oriented structure
specs/
├── login-page.spec
├── dashboard-page.spec
└── settings-page.spec

2. Use Tags Strategically

Establish a tagging convention that enables flexible test filtering in CI/CD pipelines:

Tags: regression, smoke, critical, sprint-23

Common tag categories include:

3. Keep Steps Atomic and Reusable

Write steps that do one thing and can be reused across multiple scenarios:

# Good: Atomic, reusable steps
* Open the login page
* Enter username "test@user.com"
* Enter password "Secret123"
* Click the login button

# Avoid: Overly specific, non-reusable steps
* Log in as test@user.com with password Secret123 and verify success

Atomic steps become building blocks you can recombine endlessly across different scenarios.

4. Use Data Tables for Variations, Not Separate Scenarios

When testing multiple input combinations, prefer data tables over duplicating scenarios:

# Good: One scenario with a data table
## Validate email field with various inputs

| emailInput          | shouldAccept |
|---------------------|--------------|
| user@example.com    | true         |
| user@.com           | false        |
| @example.com        | false        |
| user@example        | false        |
| ""                  | false        |

* Open registration page
* Enter email <emailInput>
* Submit the form
* Verify acceptance status is <shouldAccept>

5. Implement Robust Step Matching

Gauge uses fuzzy text matching for steps. Keep step text consistent but avoid being overly rigid. These all match the same implementation:

* Open the login page
* open the login page
* Open Login Page

However, for parameters, be explicit. Use angle brackets for dynamic values and quotes for literal strings that should be captured:

* Enter username  and password 
* Verify the message is "Successfully logged in"

6. Separate Test Data from Step Implementations

Don't hardcode test data in your step implementations. Use environment properties, external data files, or table-driven specs:

// Good: Data from environment properties
@Step("Connect to the database")
public void connectToDatabase() {
    String dbHost = Gauge.readEnvironmentProperty("db_host");
    String dbUser = Gauge.readEnvironmentProperty("db_user");
    String dbPass = Gauge.readEnvironmentProperty("db_password");
    connection = DriverManager.getConnection(dbHost, dbUser, dbPass);
}

// Avoid: Hardcoded values
@Step("Connect to the database")
public void connectToDatabase() {
    connection = DriverManager.getConnection(
        "jdbc:mysql://localhost:3306/testdb", "admin", "password123");
}

7. Leverage Parallel Execution Early

Design your tests to be independent from the start. Each scenario should set up its own state and clean up after itself. This enables parallel execution without complex refactoring later:

@BeforeScenario
public void setup() {
    // Each scenario gets a fresh browser instance
    driver = new ChromeDriver();
    // Create unique test data for this scenario
    testUser = "user_" + UUID.randomUUID().toString().substring(0, 8);
}

@AfterScenario
public void cleanup() {
    driver.quit();
    // Delete any data created during this scenario
    apiClient.deleteUser(testUser);
}

8. Write Specs That Non-Developers Can Read

The true power of Gauge comes when your specs serve as living documentation. Write them so that a product manager can understand the expected behavior without reading code:

# Shopping Cart Behavior

## Adding items to an empty cart

Tags: cart, smoke

This scenario verifies that a user can add a product to their
shopping cart and see the correct item count and subtotal update.

* Navigate to the product catalog
* Select the product "Wireless Headphones"
* Verify the product detail page shows price "$79.99"
* Click the "Add to Cart" button
* Verify the cart icon shows count "1"
* Verify the cart subtotal displays "$79.99"

## Removing the only item from the cart

Tags: cart, regression

After adding an item, the user should be able to remove it
and see the cart return to an empty state.

* Navigate to the product catalog
* Select the product "Wireless Headphones"
* Click the "Add to Cart" button
* Open the cart dropdown
* Click the "Remove" button for "Wireless Headphones"
* Verify the cart icon shows count "0"
* Verify the cart displays the message "Your cart is empty"

Notice the descriptive paragraph explaining the business context. This makes the spec file self-documenting.

9. Use the Gauge Report Generator

Enable the HTML report plugin in manifest.json and generate detailed reports after each run:

gauge run --html specs/

Reports include pass/fail status per scenario and per table row, execution time, embedded screenshots, and custom messages. These reports can be published to CI/CD artifacts for stakeholder review.

10. Integrate Gauge into CI/CD Pipelines

Gauge returns standard exit codes (0 for success, non-zero for failure) and produces JUnit-compatible XML output, making CI integration straightforward. A typical pipeline stage:

# Example CI pipeline step (Jenkins/GitHub Actions/GitLab CI)
gauge run --env ci --parallel -n 4 --tags "smoke" specs/
if [ $? -ne 0 ]; then
  echo "Smoke tests failed — blocking deployment"
  exit 1
fi

For Jenkins, use the JUnit plugin to consume reports/xml/ output. For GitHub Actions, upload the HTML report as an artifact.

Common Pitfalls and How to Avoid Them

Pitfall 1: Steps That Are Too Granular

While atomic steps are good, steps that are too tiny create noisy specs:

# Too granular — tedious to read and maintain
* Open browser
* Maximize window
* Type "https://example.com" in address bar
* Press Enter key
* Wait for page load
* Locate username field
* Clear username field
* Type "user@test.com" in username field

Instead, group logical actions:

# Better granularity — still atomic but meaningful
* Open the login page
* Enter username "user@test.com"
* Enter password "Secret123"
* Click the login button

Pitfall 2: State Leakage Between Scenarios

Gauge creates a fresh step implementation instance per scenario, but global/static state can leak. Always reset shared state in @BeforeScenario hooks. Avoid static WebDriver instances; use instance fields instead.

Pitfall 3: Ignoring the Validation Command

Run gauge validate regularly — ideally as a pre-commit hook or CI check. It catches orphaned implementations and missing steps before they cause mysterious runtime failures.

Pitfall 4: Over-Engineering Step Implementations

Step implementations should be thin adapters between spec text and your automation

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles