Background Steps
Background steps provide a way to share common setup across all scenarios in a feature. They run before each scenario, ensuring consistent starting conditions.
Basic Syntax
Use the Background keyword before your scenarios:
Feature: Shopping Cart
Background:
Given I am a logged-in customer
And I have an empty cart
Scenario: Add item to cart
When I add "Widget" to the cart
Then I should see 1 item in my cart
Scenario: Add multiple items
When I add "Widget" to the cart
And I add "Gadget" to the cart
Then I should see 2 items in my cartBoth scenarios start with a logged-in customer and an empty cart.
How Background Works
Execution Order
For each scenario in a feature:
- Fresh context is created
- All Background steps execute
- Scenario steps execute
- Context is discarded
Feature with Background + 2 Scenarios:
Scenario 1:
[New Context] → Background Steps → Scenario 1 Steps → [Discard]
Scenario 2:
[New Context] → Background Steps → Scenario 2 Steps → [Discard]Fresh Context
Each scenario gets a completely fresh context. Changes made during one scenario don't affect others:
Feature: Counter
Background:
Given the counter is 0
Scenario: First increment
When I increment the counter
Then the counter should be 1 # Starts from 0
Scenario: Second increment
When I increment the counter
Then the counter should be 1 # Also starts from 0, not 1!Multiple Background Steps
You can have multiple steps in a Background:
Feature: Admin Dashboard
Background:
Given the database is seeded
And a user "admin@test.com" exists
And the user has role "administrator"
And I am logged in as "admin@test.com"
Scenario: View users
When I visit the users page
Then I should see a list of users
Scenario: Create user
When I create a new user
Then the user count should increase by 1Step Definitions for Background
Background steps use the same step definitions as regular steps:
use TestFlowLabs\PestTestAttributes\Given;
class SetupSteps
{
#[Given('the database is seeded')]
public function seedDatabase(): void
{
$this->artisan('db:seed');
}
#[Given('I am logged in as {email}')]
public function loggedInAs(string $email): void
{
$user = User::where('email', $email)->first();
$this->actingAs($user);
}
#[Given('I have an empty cart')]
public function emptyCart(): Cart
{
return new Cart();
}
}Context Injection from Background
Return values from Background steps are available in scenario steps:
Feature: User Orders
Background:
Given a user "John" exists
And the user has an active subscription
Scenario: Place order
When the user places an order # Can inject User
Then the order should be confirmed#[Given('a user {name} exists')]
public function userExists(string $name): User
{
return User::factory()->create(['name' => $name]);
}
#[Given('the user has an active subscription')]
public function activeSubscription(User $user): Subscription
{
// $user is injected from previous background step
return Subscription::factory()
->for($user)
->active()
->create();
}
#[When('the user places an order')]
public function placeOrder(User $user, Subscription $subscription): Order
{
// Both are available from background
return Order::factory()
->for($user)
->create();
}Multi-Language Background
Background has keywords in all 70+ Gherkin languages:
Background:
Given I am logged in# language: tr
Geçmiş:
Diyelim ki sisteme giriş yaptım# language: de
Grundlage:
Angenommen ich bin eingeloggt# language: fr
Contexte:
Soit je suis connecté# language: es
Antecedentes:
Dado que estoy conectadoBest Practices
Keep Background Short
Background should contain only essential shared setup:
Good:
Background:
Given I am a logged-in customerAvoid:
Background:
Given I am on the home page
And I click "Login"
And I enter username "john@test.com"
And I enter password "secret"
And I click "Submit"
And I wait for dashboard to load
And I dismiss the welcome popupUse for Common Prerequisites
Background is ideal for:
- Authentication state
- Database seeding
- Common entity creation
- Application state setup
Don't Overuse
If only some scenarios need certain setup, don't put it in Background:
# Instead of this:
Background:
Given a user exists
And the user is an admin # Only needed for some scenarios!
# Do this:
Scenario: Admin can delete users
Given a user exists
And the user is an admin
When I delete a user
...
Scenario: User can view profile
Given a user exists # No admin needed here
When I view my profile
...Background vs Shared Steps
Sometimes a shared step is cleaner than background:
# Using Background
Background:
Given I am logged in as an admin
Scenario: Admin task 1
When I do something admin-y
...
# Alternative: Explicit in each scenario
Scenario: Admin task 1
Given I am logged in as an admin
When I do something admin-y
...Choose based on:
- Background: When ALL scenarios need it
- Explicit steps: When clarity is more important
Complete Example
Feature: E-commerce Checkout
As a customer
I want to complete a purchase
So that I can receive my products
Background:
Given I am a registered customer
And I have items in my cart:
| product | quantity |
| Wireless Mouse | 1 |
| USB Cable | 2 |
And my shipping address is set
@smoke
Scenario: Successful checkout with credit card
When I proceed to checkout
And I select "Credit Card" as payment method
And I enter valid card details
And I confirm the order
Then I should see an order confirmation
And I should receive a confirmation email
Scenario: Checkout with insufficient stock
Given "USB Cable" has only 1 item in stock
When I proceed to checkout
Then I should see a stock warning
And I should be able to adjust my cart
Scenario: Apply discount code
When I proceed to checkout
And I apply discount code "SAVE10"
Then my total should be reduced by 10%class CheckoutSteps
{
private Cart $cart;
private ?Order $order = null;
#[Given('I am a registered customer')]
public function registeredCustomer(): User
{
return User::factory()->create();
}
#[Given('I have items in my cart:')]
public function itemsInCart(User $user, TableNode $table): Cart
{
$this->cart = new Cart($user);
foreach ($table->getRows() as $row) {
$product = Product::where('name', $row['product'])->first();
$this->cart->add($product, (int) $row['quantity']);
}
return $this->cart;
}
#[Given('my shipping address is set')]
public function shippingAddressSet(User $user): void
{
Address::factory()->for($user)->create();
}
#[When('I proceed to checkout')]
public function proceedToCheckout(): void
{
$this->order = $this->cart->startCheckout();
}
#[Then('I should see an order confirmation')]
public function shouldSeeConfirmation(): void
{
expect($this->order->status)->toBe('confirmed');
}
}