Using Cucumber Scenario Outline to Test Email Validation Without Duplication
Collapse repetitive email-validation scenarios into one Cucumber Scenario Outline — a worked example with Cucumber Expressions, per-row reporting, and the trade-offs of table-driven tests.
14 May 2026, 23:15 UTC

You are validating the email field on a signup form. The address needs an @, a domain, no doubled symbols — and every rule deserves its own test. Write each rule as a separate Gherkin scenario and by the fourth case the feature file is mostly repetition: the same three steps with only the input and the expected error changing.
The useful takeaway: Cucumber's Scenario Outline collapses those near-identical scenarios into one template plus an Examples table. Cucumber runs the template once per row, and each row is reported as an independent scenario with its own pass or fail result. You keep named, readable cases without maintaining five copies of the same steps.
One template instead of five scenarios
Gherkin is the plain-text syntax Cucumber parses, and Scenario Outline is one of its long-stable keywords. You write the scenario once, marking the varying parts with angle-bracket placeholders such as <email>, then attach an Examples table where each row supplies values for those placeholders. Cucumber substitutes the row's values into every step — including data tables and doc strings — and executes the result as a standalone scenario.
Two properties make this more than a copy-paste shortcut. Setup and teardown hooks run per row, so no row inherits state from the one before it. And a failing row fails alone: the report points at the exact input that broke, not at a whole block of cases.
Worked example: rejecting bad signup emails
Here is the outline for the signup form. The table pairs each invalid input with the error message the form should show — the messages below are illustrative; use whatever your product actually displays:
Feature: Signup email validation
Scenario Outline: Signup rejects invalid email addresses
Given I am on the signup page
When I attempt to sign up with "<email>"
Then I see the error "<message>"
Examples:
| email | message |
| user@ | Enter a domain after the @ |
| user@@example.com | Only one @ is allowed |
| user.example.com | The address must contain an @ |
The table cells hold bare values; the quotes live in the step text, so a cell value of user@ substitutes to ... with "user@". The step definitions capture those quoted values with Cucumber Expressions, Cucumber's pattern language for steps. The {string} parameter type matches quoted text and hands it to your method as a String:
import io.cucumber.java.en.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class SignupSteps {
@When("I attempt to sign up with {string}")
public void attemptSignup(String email) {
signupPage.enterEmail(email);
signupPage.submit();
}
@Then("I see the error {string}")
public void seeError(String expectedMessage) {
assertEquals(expectedMessage, signupPage.errorMessage());
}
}
signupPage stands in for whatever driver or client code your project uses — the point is that step definitions stay thin and business-level, so the same expressions serve plain scenarios and outlines alike. Built-in parameter types such as {int} and {double} work the same way when a table carries numbers.
On reporting: each Examples row appears as its own scenario in the output, typically labelled with the row's values or an index, depending on the Cucumber implementation and formatter. Deliberately break one row and run with a verbose formatter to confirm the report names the failing input.
The trade-off: tables that invite too much
Example tables make it cheap to add cases, which is exactly their danger. A grid that tries every combination of malformed input bloats runtime and hides intent — a reader can no longer tell which rule each row protects. Prefer a handful of meaningful, named cases, and add rows when a real bug justifies them.
Runtime is the second cost. Hooks and any browser or container setup fire once per row, so a ten-row outline behind a slow end-to-end setup costs ten setups. Tag the outline (for example @email-validation) so CI can filter or shard runs, and check whether your runner offers a once-per-suite hook — recent Cucumber-JVM versions support static @BeforeAll methods for expensive shared resources; verify against your version's documentation.
Two smaller gotchas. Angle brackets are plain-text substitution, so avoid literal <...> in step wording that could collide with a placeholder. And keep each row to one behavior: if a row's step fails midway, later steps for that input are skipped, so a row asserting five things tells you less than three focused rows.
Verify it on your own stack
Report wording differs across Cucumber implementations and versions, so confirm the behavior locally before relying on it:
- Create a minimal project with one outline and two Examples rows, and confirm two separate results appear.
- With the JavaScript implementation, run
npx cucumber-js --format progressfrom the project root; on the JVM, enable the pretty plugin in your runner options. Check that each row is listed as its own scenario. - Make one row fail on purpose and confirm the report identifies that row's input.
- Add a tagged hook and confirm it fires once per row, then scope it with the outline's tag to keep expensive setup under control.
Reach for an outline whenever three or more scenarios differ only by input and expected result. Keep the table about behavior — what should happen — and keep the how in the step definitions, and the feature file stays readable as the case list grows.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.