Skip to main content

Chapter 10: Behavioral Specifications


Learning Objectives

By the end of this chapter, you will be able to:

  • Write behavioral specifications in Given/When/Then (BDD/Gherkin) format
  • Distinguish behavioral specs from implementation specs
  • Create scenarios for happy paths, error paths, and edge cases
  • Use Scenario Outlines for parameterized testing
  • Structure feature files with Feature, Background, and Scenario
  • Translate acceptance criteria into executable Gherkin scenarios
  • Integrate behavioral specs with Cucumber, Playwright BDD, and Behave
  • Avoid anti-patterns that test implementation instead of behavior

What Are Behavioral Specifications?

A behavioral specification describes what the system does from the user's perspective, not how it does it. It answers: "When a user does X in context Y, what happens?" — in a format that serves simultaneously as documentation and as executable test definitions.

Think of it like a flight manual. The manual doesn't say "the hydraulic pump activates the actuator which moves the control surface." It says: "When you pull back on the yoke, the nose rises." Pilots (and testers) care about behavior. Engineers care about implementation. Behavioral specs speak to behavior.

In software, the same principle applies. "When the user clicks 'Add to Cart' with a valid product, the cart shows the product and updates the total" is a behavioral specification. "The AddToCartButton component calls the cartReducer with ADD_ITEM action" is an implementation detail. Behavioral specs survive refactoring; implementation specs break when you change the architecture.


The Given/When/Then Format

Behavioral specifications use a three-part structure:

  • Given — The preconditions; the state of the world before the action
  • When — The action; what the user (or system) does
  • Then — The expected outcome; what should happen

This format is known as Gherkin, the language used by Cucumber and many BDD (Behavior-Driven Development) tools. It reads like natural language but has precise semantics for test automation.

Basic Structure

Given [precondition 1]
And [precondition 2]
When [user action]
Then [expected result 1]
And [expected result 2]

Example: Login

Scenario: Successful login with valid credentials
Given a user exists with email "alice@example.com" and password "SecurePass123"
And the user is on the login page
When the user enters "alice@example.com" in the email field
And the user enters "SecurePass123" in the password field
And the user clicks the "Log in" button
Then the user is redirected to the dashboard
And the user sees "Welcome, Alice"

Why This Format Works

  1. Unambiguous: Given/When/Then forces you to separate preconditions, actions, and outcomes. No mixing of concerns.

  2. Testable: Each scenario maps directly to an automated test. Tools like Cucumber parse Gherkin and execute step definitions.

  3. Readable: Stakeholders can read and validate scenarios without knowing how to code.

  4. Stable: Behavior rarely changes; implementation changes often. Scenarios survive refactors.


Feature File Structure

A Gherkin feature file has a standard structure:

Feature: [Feature name]
[Optional description - 1-3 lines explaining the feature]

[Optional] Background:
[Steps that run before every scenario in this feature]

Scenario: [Scenario name]
[Steps]

Scenario: [Another scenario]
[Steps]

Scenario Outline: [Parameterized scenario name]
[Steps with <placeholders>]
Examples:
| col1 | col2 |
| val1 | val2 |

Feature

The Feature keyword introduces the feature and its description. The description provides context for all scenarios in the file.

Feature: Shopping Cart
Users can add products to a cart, update quantities, remove items,
and proceed to checkout. The cart persists across sessions for
logged-in users.

Background

Background contains steps that run before every scenario. Use it for common setup that every scenario needs.

Feature: Shopping Cart

Background:
Given the store has the following products:
| name | price | sku |
| Widget A | 9.99 | WID-001|
| Widget B | 19.99 | WID-002|
And the user is logged in as "alice@example.com"

Scenario

Each Scenario is one test case. It should be independent — no scenario should depend on another's side effects.

Scenario Outline

Scenario Outline allows parameterized scenarios. You define placeholders in the steps and provide values in an Examples table. The tool generates one scenario per row.

Scenario Outline: Add product to cart with different quantities
Given the user is on the product page for "<product>"
When the user adds quantity <quantity> to cart
Then the cart shows "<product>" with quantity <quantity>
And the cart total is <expected_total>

Examples:
| product | quantity | expected_total |
| Widget A | 1 | 9.99 |
| Widget A | 3 | 29.97 |
| Widget B | 2 | 39.98 |

Writing Scenarios: Happy Path, Error Path, Edge Cases

Every feature needs scenarios for three categories:

1. Happy Path

The primary success flow. What happens when everything goes right?

Scenario: Add product to empty cart
Given the user is on the product page for "Widget A"
And the cart is empty
When the user clicks "Add to Cart"
Then the cart shows 1 item
And the cart displays "Widget A" with quantity 1
And the cart total is 9.99
And the user sees a "Added to cart" confirmation

2. Error Paths

What happens when something goes wrong? Invalid input, missing data, permission denied.

Scenario: Add to cart fails when product is out of stock
Given the user is on the product page for "Widget A"
And "Widget A" has 0 units in stock
When the user clicks "Add to Cart"
Then the user sees "Out of stock" message
And the cart is not updated
And the "Add to Cart" button is disabled

Scenario: Add to cart fails when not logged in (if required)
Given the user is not logged in
And the user is on the product page for "Widget A"
When the user clicks "Add to Cart"
Then the user is redirected to the login page
And the user sees "Please log in to add items to cart"

3. Edge Cases

Boundary conditions: empty input, maximum values, concurrent actions, expired sessions.

Scenario: Cannot add quantity exceeding stock
Given the user is on the product page for "Widget A"
And "Widget A" has 5 units in stock
When the user enters quantity 10
And the user clicks "Add to Cart"
Then the user sees "Maximum 5 available"
And the quantity field shows 5

Scenario: Cart persists after session timeout
Given the user has "Widget A" in cart with quantity 2
And the user's session has expired
When the user navigates to the cart page
Then the user is prompted to log in
And after logging in, the cart still shows "Widget A" with quantity 2

Translating Acceptance Criteria to Gherkin

Acceptance criteria from Chapter 9 map directly to Gherkin scenarios. The format is already Given/When/Then compatible.

Acceptance Criterion:

Given a valid registered email, When user submits reset request, Then user sees "Check your email" message and receives email within 60 seconds

Gherkin Scenario:

Scenario: Password reset request for valid email
Given a user exists with email "alice@example.com"
And the user is on the password reset request page
When the user enters "alice@example.com" in the email field
And the user clicks "Send reset link"
Then the user sees "Check your email" message
And an email is sent to "alice@example.com" within 60 seconds
And the email contains a password reset link

Acceptance Criterion:

Given an unregistered email, When user submits reset request, Then user sees "Check your email" message (no enumeration)

Gherkin Scenario:

Scenario: Password reset request for unregistered email (no enumeration)
Given no user exists with email "unknown@example.com"
And the user is on the password reset request page
When the user enters "unknown@example.com" in the email field
And the user clicks "Send reset link"
Then the user sees "Check your email" message
And no email is sent
And the response is identical to the valid-email case

Translation Guidelines

  1. One AC → One or more scenarios: A complex AC may need multiple scenarios (e.g., valid/invalid cases).

  2. Preserve the Given/When/Then structure: ACs often already use this structure; minimal transformation needed.

  3. Add concrete values: Replace abstract "valid email" with "alice@example.com" for testability.

  4. Split compound Thens: If an AC has multiple outcomes, you can split into multiple And clauses or separate scenarios.

  5. Add setup steps: ACs may assume preconditions; add Given steps to establish them.


Tutorial: Shopping Cart Behavioral Specs

Let's write a complete set of behavioral specifications for a Shopping Cart feature. We'll cover the full cart lifecycle: add, update, remove, persist, checkout.

Feature: Shopping Cart

Feature: Shopping Cart
Users can add products to a cart, update quantities, remove items,
and view the cart total. The cart persists for logged-in users.
Guest users have a session-based cart.

Background

  Background:
Given the store has the following products:
| name | price | sku | stock |
| Widget A | 9.99 | WID-001 | 100 |
| Widget B | 19.99 | WID-002 | 50 |
| Widget C | 4.99 | WID-003 | 0 |

Scenarios: Add to Cart

  Scenario: Add single product to empty cart
Given the user is logged in as "alice@example.com"
And the user is on the product page for "Widget A"
And the cart is empty
When the user clicks "Add to Cart"
Then the cart shows 1 item
And the cart contains "Widget A" with quantity 1
And the cart subtotal is 9.99
And the user sees "Widget A added to cart" message

Scenario: Add multiple quantity of same product
Given the user is logged in as "alice@example.com"
And the user is on the product page for "Widget A"
And the cart is empty
When the user sets quantity to 3
And the user clicks "Add to Cart"
Then the cart shows 1 item
And the cart contains "Widget A" with quantity 3
And the cart subtotal is 29.97

Scenario: Add same product twice increments quantity
Given the user is logged in as "alice@example.com"
And the user has "Widget A" with quantity 2 in the cart
When the user navigates to the product page for "Widget A"
And the user clicks "Add to Cart"
Then the cart contains "Widget A" with quantity 3
And the cart subtotal is 29.97

Scenario: Cannot add out-of-stock product
Given the user is logged in as "alice@example.com"
And the user is on the product page for "Widget C"
When the user clicks "Add to Cart"
Then the user sees "Out of stock" message
And the "Add to Cart" button is disabled
And the cart is not updated

Scenario: Cannot add quantity exceeding stock
Given the user is logged in as "alice@example.com"
And the user is on the product page for "Widget B"
And "Widget B" has 50 units in stock
When the user sets quantity to 100
And the user clicks "Add to Cart"
Then the user sees "Maximum 50 available" message
And the quantity is adjusted to 50
And the cart contains "Widget B" with quantity 50

Scenarios: Update Quantity

  Scenario: Update quantity in cart
Given the user is logged in as "alice@example.com"
And the user has "Widget A" with quantity 2 in the cart
When the user goes to the cart page
And the user changes quantity of "Widget A" to 5
And the user clicks "Update"
Then the cart contains "Widget A" with quantity 5
And the cart subtotal is 49.95

Scenario: Reduce quantity to zero removes item
Given the user is logged in as "alice@example.com"
And the user has "Widget A" with quantity 1 in the cart
When the user goes to the cart page
And the user changes quantity of "Widget A" to 0
And the user clicks "Update"
Then the cart is empty
And the user sees "Widget A removed from cart" message

Scenario: Update quantity cannot exceed stock
Given the user is logged in as "alice@example.com"
And the user has "Widget B" with quantity 10 in the cart
And "Widget B" has 50 units in stock
When the user goes to the cart page
And the user changes quantity of "Widget B" to 60
And the user clicks "Update"
Then the user sees "Maximum 50 available" message
And the cart contains "Widget B" with quantity 10

Scenarios: Remove Item

  Scenario: Remove item from cart
Given the user is logged in as "alice@example.com"
And the user has "Widget A" and "Widget B" in the cart
When the user goes to the cart page
And the user clicks "Remove" for "Widget A"
Then the cart contains only "Widget B"
And the cart subtotal is 19.99
And the user sees "Widget A removed from cart" message

Scenario: Remove last item shows empty cart
Given the user is logged in as "alice@example.com"
And the user has "Widget A" with quantity 1 in the cart
When the user goes to the cart page
And the user clicks "Remove" for "Widget A"
Then the cart is empty
And the user sees "Your cart is empty" message
And the user sees "Continue shopping" link

Scenarios: Cart Persistence

  Scenario: Cart persists across sessions for logged-in user
Given the user is logged in as "alice@example.com"
And the user has "Widget A" with quantity 2 in the cart
When the user logs out
And the user logs in as "alice@example.com"
Then the cart contains "Widget A" with quantity 2
And the cart subtotal is 19.98

Scenario: Guest cart is session-based
Given the user is not logged in
And the user has "Widget A" with quantity 1 in the cart
When the user closes the browser
And the user opens the site in a new session
Then the cart is empty

Scenario: Guest cart merges on login
Given the user is not logged in
And the user has "Widget A" with quantity 1 in the cart
And a user "alice@example.com" has "Widget B" in their saved cart
When the user logs in as "alice@example.com"
Then the cart contains "Widget A" with quantity 1
And the cart contains "Widget B" with its saved quantity

Scenario Outline: Multiple Products

  Scenario Outline: Cart total calculation for multiple items
Given the user is logged in as "alice@example.com"
And the cart is empty
When the user adds the following to cart:
| product | quantity |
| <product1> | <qty1> |
| <product2> | <qty2> |
Then the cart subtotal is <expected_total>

Examples:
| product1 | qty1 | product2 | qty2 | expected_total |
| Widget A | 1 | Widget B | 1 | 29.98 |
| Widget A | 2 | Widget B | 3 | 89.95 |
| Widget A | 0 | Widget B | 1 | 19.99 |

Anti-Pattern: Testing Implementation, Not Behavior

The most common mistake in behavioral specs is writing scenarios that test how the system works internally instead of what the user experiences.

Bad: Implementation-Coupled

Scenario: Add to cart calls API
Given the user is on the product page
When the user clicks "Add to Cart"
Then a POST request is sent to "/api/cart/items"
And the request body contains {"sku": "WID-001", "quantity": 1}
And the response status is 201

Why it's bad: This tests the API contract, not user-visible behavior. If you switch from REST to GraphQL, the scenario breaks even though the user experience is identical.

Good: Behavior-Focused

Scenario: Add to cart updates cart display
Given the user is on the product page for "Widget A"
And the cart is empty
When the user clicks "Add to Cart"
Then the cart shows 1 item
And the cart displays "Widget A" with quantity 1
And the cart total is 9.99

Why it's good: This tests what the user sees. The implementation (REST, GraphQL, local state) doesn't matter.

More Anti-Patterns

Anti-PatternBad ExampleGood Alternative
Testing UI structure"Then the div with class 'cart-item' is visible""Then the cart displays 'Widget A'"
Testing internal state"Then the Redux store has cart.items.length === 1""Then the cart shows 1 item"
Testing implementation details"Then the useEffect runs"(Don't test; test outcome)
Over-specific selectors"Then element #add-btn-123 is clicked""Then the user clicks 'Add to Cart'"
Testing third-party behavior"Then the analytics library receives event"(Test in unit/integration test)

The "User Perspective" Test

Before adding a step, ask: Would a user (or external system) know this happened? If not, it's probably implementation detail.


Integration with Test Frameworks

Behavioral specs become executable tests through step definitions that map Gherkin steps to code.

Cucumber (JavaScript/TypeScript)

// features/step_definitions/cart_steps.js
const { Given, When, Then } = require('@cucumber/cucumber');
const { expect } = require('chai');

Given('the user is logged in as {string}', async function (email) {
await this.page.goto('/login');
await this.page.fill('[name=email]', email);
await this.page.fill('[name=password]', 'password123');
await this.page.click('button[type=submit]');
await this.page.waitForURL('**/dashboard');
});

Given('the cart is empty', async function () {
await this.page.goto('/cart');
await this.page.click('button:has-text("Clear cart")');
});

When('the user clicks {string}', async function (buttonText) {
await this.page.click(`button:has-text("${buttonText}")`);
});

Then('the cart shows {int} item', async function (count) {
const cartCount = await this.page.textContent('.cart-count');
expect(parseInt(cartCount)).to.equal(count);
});

Playwright BDD

Playwright supports Gherkin via @playwright/bdd or similar. Scenarios run as Playwright tests with full browser automation.

// cart.spec.ts (Playwright + Gherkin-style)
import { test, expect } from '@playwright/test';

test('Add product to empty cart', async ({ page }) => {
// Given
await page.goto('/products/widget-a');
await expect(page.locator('.cart-count')).toHaveText('0');

// When
await page.click('button:has-text("Add to Cart")');

// Then
await expect(page.locator('.cart-count')).toHaveText('1');
await expect(page.locator('.cart-item')).toContainText('Widget A');
await expect(page.locator('.cart-total')).toContainText('9.99');
});

Behave (Python)

# features/steps/cart_steps.py
from behave import given, when, then

@given('the user is logged in as "{email}"')
def step_logged_in(context, email):
context.browser.visit('/login')
context.browser.fill('email', email)
context.browser.fill('password', 'password123')
context.browser.find_by_text('Log in').click()

@given('the cart is empty')
def step_empty_cart(context):
context.browser.visit('/cart')
context.browser.find_by_text('Clear cart').click()

@when('the user clicks "{button_text}"')
def step_click_button(context, button_text):
context.browser.find_by_text(button_text).click()

@then('the cart shows {count:d} item')
def step_cart_count(context, count):
cart_count = context.browser.find_by_css('.cart-count').text
assert int(cart_count) == count

Choosing a Framework

FrameworkLanguageBest For
CucumberJS, Java, Ruby, PythonFull BDD; stakeholder-readable specs
PlaywrightJS/TSE2E with modern browser automation
BehavePythonPython projects; pytest integration
SpecFlowC#.NET ecosystem

Step Definition Best Practices

1. One Concept Per Step

Bad: "Given the user is logged in and has Widget A in cart and the cart total is 9.99"

Good: Split into separate steps or use a data table.

2. Reusable Steps

Write steps that can be used across scenarios. "The user clicks {string}" is reusable. "The user clicks the Add to Cart button on the Widget A product page" is too specific.

3. Avoid Brittle Selectors

Bad: page.locator('#react-root > div > main > div.cart-item-0 > span')

Good: page.locator('[data-testid="cart-item"]') or page.getByRole('button', { name: 'Add to Cart' })

4. Use Data Tables for Complex Setup

Given the store has the following products:
| name | price | sku |
| Widget A | 9.99 | WID-001 |
| Widget B | 19.99 | WID-002 |

5. Keep Steps Implementation-Agnostic

Steps should not assume REST vs. GraphQL, React vs. Vue, etc. They describe user-visible behavior.


Relationship to Acceptance Criteria

Behavioral specs and acceptance criteria are two representations of the same requirements:

Acceptance Criteria (in spec doc)     Behavioral Specs (in .feature file)
──────────────────────────────── ─────────────────────────────────
Given X, When Y, Then Z ←→ Scenario: [Name]
Given X
When Y
Then Z

Workflow:

  1. Write acceptance criteria in the specification document (Chapter 9 format).
  2. Create a .feature file and translate each AC into one or more scenarios.
  3. Implement step definitions that execute the scenarios.
  4. Run scenarios as part of CI/CD.

The feature file becomes the single source of truth for behavior. The spec document provides context (problem, constraints, NFRs); the feature file provides executable validation.


Try With AI

Prompt 1: Acceptance Criteria to Gherkin

"I have these acceptance criteria: [paste ACs]. Convert each into a Gherkin scenario. Use concrete values (e.g., 'alice@example.com' instead of 'valid email'). Ensure each scenario is independent and includes necessary Given steps for setup. Output a complete .feature file."

Prompt 2: Anti-Pattern Review

"Review these Gherkin scenarios for implementation-coupling: [paste scenarios]. Identify any steps that test implementation (API calls, internal state, DOM structure) rather than user-visible behavior. Suggest behavior-focused replacements for each."

Prompt 3: Scenario Completeness

"I have a feature: [describe feature]. Generate a complete set of Gherkin scenarios covering: (1) happy path, (2) all error paths you can identify, (3) edge cases (empty, max values, concurrent actions). Use Scenario Outline where appropriate for parameterized cases."

Prompt 4: Step Definition Generation

"I have these Gherkin steps: [paste steps]. Generate Playwright step definitions in TypeScript. Use page.getByRole and data-testid for selectors. Assume a standard e-commerce cart UI. Make steps reusable."


Practice Exercises

Exercise 1: Translate and Execute

Take the Team Invitation acceptance criteria from Chapter 9. Translate them into a complete .feature file. Then implement step definitions using your preferred framework (Cucumber, Playwright, Behave). Run the scenarios. Fix any failures.

Expected outcome: A passing feature file that validates the Team Invitation behavior. You will learn the translation from AC → Gherkin and the step definition pattern.

Exercise 2: Behavior vs. Implementation

Take an existing test suite (unit or E2E) from a project. Identify tests that are implementation-coupled. Rewrite 3 of them as behavioral scenarios. Compare: what did you have to change? What became simpler?

Expected outcome: Behavioral scenarios that are shorter, more stable, and focused on user outcomes. You will see the difference between "testing the code" and "testing the behavior."

Exercise 3: Scenario Outline Design

Design a Scenario Outline for a "User Registration" feature that tests validation for: (a) invalid email formats, (b) weak passwords, (c) password mismatch. Create an Examples table with 6+ rows covering different invalid inputs and expected error messages.

Expected outcome: A parameterized scenario that reduces duplication and makes it easy to add new validation cases.


Key Takeaways

  1. Behavioral specs describe what the system does from the user's perspective, not how it does it. They survive refactoring.

  2. Given/When/Then (Gherkin) provides a standard format that is human-readable and machine-executable. Each scenario maps to a test.

  3. Feature files use Feature, Background, Scenario, and Scenario Outline. Background runs before every scenario; Scenario Outline enables parameterized testing.

  4. Three scenario types: Happy path (success), error paths (failures), edge cases (boundaries). All three are necessary for complete coverage.

  5. Acceptance criteria translate directly to Gherkin. The spec document and feature file are two views of the same requirements.

  6. Avoid implementation-coupled scenarios. Test user-visible outcomes, not API calls, internal state, or DOM structure. Use the "user perspective" test.

  7. Step definitions bridge Gherkin to code. Use reusable steps, implementation-agnostic language, and stable selectors (roles, test IDs).


Chapter Quiz

  1. What is the difference between a behavioral specification and an implementation specification? Give an example of each for a "Add to Cart" feature.

  2. Explain the purpose of each part of Given/When/Then. Why is it important to keep them separate?

  3. When should you use a Scenario Outline instead of multiple separate Scenarios?

  4. What is the purpose of the Background section in a feature file? When would you not use it?

  5. Translate this acceptance criterion into a Gherkin scenario: "Given a user with an expired session, When the user adds an item to cart, Then the user is prompted to log in and the item is added after login."

  6. Why is "Then a POST request is sent to /api/cart" an anti-pattern in behavioral specs? What would a behavior-focused alternative be?

  7. Name three step definition best practices for maintaining stable, reusable scenarios.

  8. How do behavioral specs relate to acceptance criteria? What is the recommended workflow for keeping them in sync?