Scenario Outlines
Scenario Outlines (also called Scenario Templates) allow you to run the same scenario multiple times with different data. They're perfect for data-driven testing.
Basic Syntax
Use Scenario Outline with an Examples table:
Feature: Calculator
Scenario Outline: Addition with various inputs
Given I have number <start>
When I add <add>
Then the result should be <result>
Examples:
| start | add | result |
| 5 | 3 | 8 |
| 10 | 5 | 15 |
| 0 | 0 | 0 |
| -5 | 10 | 5 |This generates 4 separate test cases, one for each row in the Examples table.
How It Works
Placeholder Substitution
Placeholders in angle brackets <name> are replaced with values from the Examples table:
Given I have number <start>
# Row 1: Given I have number 5
# Row 2: Given I have number 10
# Row 3: Given I have number 0
# Row 4: Given I have number -5Generated Tests
Each Examples row creates an independent test:
PASS Feature: Calculator
✓ Scenario Outline: Addition with various inputs (Example 1)
✓ Given I have number 5
✓ When I add 3
✓ Then the result should be 8
✓ Scenario Outline: Addition with various inputs (Example 2)
✓ Given I have number 10
✓ When I add 5
✓ Then the result should be 15
...Step Definitions
Step definitions work exactly the same—the placeholders are replaced before matching:
#[Given('I have number {n}')]
public function haveNumber(int $n): void
{
$this->number = $n;
}
#[When('I add {n}')]
public function add(int $n): void
{
$this->number += $n;
}
#[Then('the result should be {expected}')]
public function resultShouldBe(int $expected): void
{
expect($this->number)->toBe($expected);
}String Values in Examples
For string values, you can include or exclude quotes:
Scenario Outline: User greeting
Given a user "<name>" exists
Then the greeting should be "<greeting>"
Examples:
| name | greeting |
| John | Hello, John! |
| Jane | Hello, Jane! |The quotes in the step are part of the pattern, not the placeholder.
Multiple Examples Tables
You can have multiple Examples tables, often to group related data:
Scenario Outline: User permissions
Given a user with role "<role>"
When they try to access "<resource>"
Then access should be "<result>"
Examples: Admin permissions
| role | resource | result |
| admin | dashboard | granted |
| admin | settings | granted |
| admin | users | granted |
Examples: Regular user permissions
| role | resource | result |
| user | dashboard | granted |
| user | settings | denied |
| user | users | denied |Each Examples table can have a name for documentation.
Tags on Examples
Apply tags to specific Examples tables:
Scenario Outline: Payment processing
Given I have <amount> in my account
When I purchase an item for <price>
Then my balance should be <balance>
@smoke
Examples: Basic transactions
| amount | price | balance |
| 100 | 50 | 50 |
| 200 | 75 | 125 |
@slow @integration
Examples: Edge cases
| amount | price | balance |
| 0 | 0 | 0 |
| 1000 | 1000 | 0 |Run only smoke examples:
pest --bdd --tags="@smoke"Complex Data Types
Boolean Values
Scenario Outline: Feature flags
Given feature "<feature>" is <enabled>
Then the feature should be <status>
Examples:
| feature | enabled | status |
| dark_mode | true | visible |
| dark_mode | false | hidden |
| beta_panel | yes | visible |
| beta_panel | no | hidden |Float Values
Scenario Outline: Discount calculation
Given a product priced at <price>
When I apply a <discount>% discount
Then the final price should be <final>
Examples:
| price | discount | final |
| 100.00 | 10 | 90.00 |
| 50.00 | 25 | 37.50 |
| 75.50 | 20 | 60.40 |Best Practices
Meaningful Example Names
Use descriptive Examples table names:
Scenario Outline: Login validation
Given I enter email "<email>"
And I enter password "<password>"
When I submit the login form
Then I should see "<message>"
Examples: Valid credentials
| email | password | message |
| user@example.com | secret | Welcome back! |
Examples: Invalid email formats
| email | password | message |
| not-an-email | secret | Invalid email |
| @missing.com | secret | Invalid email |
Examples: Password requirements
| email | password | message |
| user@example.com | | Password required |
| user@example.com | short | Password too short |Keep Tables Focused
Each Examples table should test related variations:
Good:
Examples: Positive numbers
| a | b | sum |
| 1 | 2 | 3 |
| 5 | 5 | 10 |
Examples: Negative numbers
| a | b | sum |
| -1 | -2 | -3 |
| -5 | 10 | 5 |Avoid: One giant table mixing unrelated test cases.
Use When Data Varies, Not Logic
Scenario Outlines are for varying data, not behavior:
Good use case:
Scenario Outline: Currency conversion
Given I have <amount> <from_currency>
When I convert to <to_currency>
Then I should have approximately <result> <to_currency>
Examples:
| amount | from_currency | to_currency | result |
| 100 | USD | EUR | 92 |
| 100 | EUR | GBP | 86 |Better as separate scenarios:
# Different logic paths shouldn't be in outlines
Scenario: Login with valid credentials
Given a valid user
When I login
Then I should see dashboard
Scenario: Login with invalid credentials
Given an invalid user
When I login
Then I should see error # Different outcome, different logicComplete Example
Feature: E-commerce Pricing
As a customer
I want to see accurate pricing
So I can make informed purchases
Scenario Outline: Product pricing with tax
Given a product "<product>" priced at $<base_price>
And the tax rate is <tax_rate>%
When I view the product
Then the displayed price should be $<total>
Examples: US Tax Rates
| product | base_price | tax_rate | total |
| Wireless Mouse | 29.99 | 8.25 | 32.46 |
| USB Cable | 9.99 | 8.25 | 10.81 |
| Keyboard | 79.99 | 8.25 | 86.59 |
Examples: EU Tax Rates
| product | base_price | tax_rate | total |
| Wireless Mouse | 29.99 | 20 | 35.99 |
| USB Cable | 9.99 | 20 | 11.99 |
| Keyboard | 79.99 | 20 | 95.99 |
Scenario Outline: Quantity discounts
Given I add <quantity> of "<product>" to my cart
When the discount is calculated
Then I should receive <discount>% off
Examples:
| quantity | product | discount |
| 1 | Wireless Mouse | 0 |
| 5 | Wireless Mouse | 5 |
| 10 | Wireless Mouse | 10 |
| 25 | Wireless Mouse | 15 |class PricingSteps
{
private Product $product;
private float $taxRate;
private Cart $cart;
#[Given('a product {name} priced at ${price}')]
public function productWithPrice(string $name, float $price): Product
{
return $this->product = Product::factory()->create([
'name' => $name,
'price' => $price,
]);
}
#[Given('the tax rate is {rate}%')]
public function taxRate(float $rate): void
{
$this->taxRate = $rate;
}
#[Then('the displayed price should be ${expected}')]
public function displayedPriceShouldBe(float $expected): void
{
$calculated = $this->product->price * (1 + $this->taxRate / 100);
expect(round($calculated, 2))->toBe($expected);
}
#[Given('I add {quantity} of {product} to my cart')]
public function addToCart(int $quantity, string $product): void
{
$this->cart = new Cart();
$item = Product::where('name', $product)->first();
$this->cart->add($item, $quantity);
}
#[Then('I should receive {discount}% off')]
public function shouldReceiveDiscount(int $discount): void
{
expect($this->cart->discountPercentage())->toBe($discount);
}
}