Multiple Instances
When a scenario needs multiple instances of the same model, use aliases to distinguish them. This allows you to create and reference multiple users, orders, or any other model independently.
Alias Syntax
Add as {aliasName} to the end of a Given step:
Scenario: Two users comparison
Given a user "John" exists as {firstUser}
Given a user "Jane" exists as {secondUser}
When I compare the users
Then firstUser should not equal secondUserThe alias name in curly braces becomes the parameter name for injection.
How Aliases Work
Without Alias (Single Instance)
Given a user "John" exists
Given a user "Jane" exists
# Only "Jane" is available - overwrote "John"When you don't use aliases, later steps overwrite earlier ones of the same type.
With Alias (Multiple Instances)
Given a user "John" exists as {john}
Given a user "Jane" exists as {jane}
# Both are available: john and janeWith aliases, each instance is stored separately and can be accessed by its alias name.
Injection by Alias
Access aliased models by matching parameter names:
#[Then('firstUser should not equal secondUser')]
public function usersNotEqual(User $firstUser, User $secondUser): void
{
expect($firstUser->id)->not->toBe($secondUser->id);
}
#[When('I transfer from john to jane')]
public function transfer(User $john, User $jane): void
{
$john->transferCreditsTo($jane, 100);
}The parameter name must match the alias exactly.
Complete Example
Feature File
Feature: Money Transfer
As a user
I want to transfer money to another user
So that I can pay them for services
Scenario: Successful transfer
Given a user "Alice" exists as {sender}
And a user with 1000 credits exists as {sender}
Given a user "Bob" exists as {receiver}
And a user with 0 credits exists as {receiver}
When I transfer 500 credits from sender to receiver
Then sender should have 500 credits
And receiver should have 500 credits
Scenario: Insufficient funds
Given a user "Alice" exists as {sender}
And a user with 100 credits exists as {sender}
Given a user "Bob" exists as {receiver}
When I try to transfer 500 credits from sender to receiver
Then the transfer should fail with "Insufficient funds"
And sender should have 100 credits
And receiver should have 0 creditsStep Definitions
class TransferSteps
{
private ?TransferResult $result = null;
#[When('I transfer {amount} credits from sender to receiver')]
public function transfer(int $amount, User $sender, User $receiver): void
{
$this->result = TransferService::transfer($sender, $receiver, $amount);
}
#[When('I try to transfer {amount} credits from sender to receiver')]
public function tryTransfer(int $amount, User $sender, User $receiver): void
{
try {
$this->result = TransferService::transfer($sender, $receiver, $amount);
} catch (InsufficientFundsException $e) {
$this->result = TransferResult::failed($e->getMessage());
}
}
#[Then('sender should have {credits} credits')]
public function senderCredits(int $credits, User $sender): void
{
expect($sender->fresh()->credits)->toBe($credits);
}
#[Then('receiver should have {credits} credits')]
public function receiverCredits(int $credits, User $receiver): void
{
expect($receiver->fresh()->credits)->toBe($credits);
}
#[Then('the transfer should fail with {message}')]
public function transferFailed(string $message): void
{
expect($this->result->failed)->toBeTrue();
expect($this->result->message)->toBe($message);
}
}Chaining States with Aliases
Aliases work with state chaining. All Given steps with the same alias contribute to the same model:
Scenario: Complex user setup with alias
Given a user "Admin Alice" exists as {admin}
And an admin user exists as {admin}
And a verified user exists as {admin}
Given a user "Regular Bob" exists as {regular}
And a user with 50 credits exists as {regular}
When admin reviews regular's account
Then admin should see Bob with 50 creditsBoth chains build up independently:
{admin}: User named "Admin Alice" + admin role + verified{regular}: User named "Regular Bob" + 50 credits
Common Patterns
Comparison Testing
Scenario: Compare premium vs regular user access
Given a user "Premium User" exists as {premium}
And a premium user exists as {premium}
Given a user "Regular User" exists as {regular}
When both users try to access premium features
Then premium should have access
And regular should not have accessRelationship Testing
Scenario: Follow relationship
Given a user "Alice" exists as {follower}
Given a user "Bob" exists as {followed}
When follower follows followed
Then followed should have 1 follower
And follower should be following 1 userParent-Child Relationships
Scenario: Organization hierarchy
Given a user "CEO" exists as {ceo}
And an admin user exists as {ceo}
Given a user "Manager" exists as {manager}
And a manager user exists as {manager}
Given a user "Employee" exists as {employee}
When I build the org chart
Then ceo should be at the top
And manager should report to ceo
And employee should report to managerTransaction Testing
Scenario: Order between buyer and seller
Given a user "Buyer" exists as {buyer}
And a user with 1000 credits exists as {buyer}
Given a user "Seller" exists as {seller}
And a merchant user exists as {seller}
Given a product "Widget" exists as {product}
And a product with price 100 exists as {product}
When buyer purchases product from seller
Then buyer should have 900 credits
And seller should have received 100 credits
And product should be marked as soldMultiple Instances of Different Types
You can alias different model types:
Scenario: Complex e-commerce flow
Given a user "John" exists as {customer}
Given a user "Admin" exists as {admin}
And an admin user exists as {admin}
Given a product "Widget" exists as {mainProduct}
And a product with price 50 exists as {mainProduct}
Given a product "Accessory" exists as {accessory}
And a product with price 10 exists as {accessory}
When customer adds mainProduct to cart
And customer adds accessory to cart
Then the cart total should be 60Without Factory Integration
You can also use aliases with regular step definitions:
class UserSteps
{
#[Given('a user {name} exists')]
public function userExists(string $name): User
{
return User::factory()->create(['name' => $name]);
}
}The alias is added in the feature file, not the step definition:
Given a user "John" exists as {john}
# The returned User is stored with alias "john"Best Practices
Use Descriptive Alias Names
# Good: Clear intent
Given a user "John" exists as {buyer}
Given a user "Jane" exists as {seller}
# Avoid: Generic names
Given a user "John" exists as {user1}
Given a user "Jane" exists as {user2}Match Alias to Role
# Good: Alias reflects the user's role in the scenario
Given a user "Alice" exists as {reviewer}
Given a user "Bob" exists as {author}
When reviewer reviews author's submission
# The step definition is clear:
#[When('reviewer reviews author\'s submission')]
public function review(User $reviewer, User $author): voidKeep Alias Count Manageable
If you need many aliased instances, consider if the scenario is too complex:
# Consider splitting if you have many aliases
Given a user exists as {user1}
Given a user exists as {user2}
Given a user exists as {user3}
Given a user exists as {user4}
Given a user exists as {user5}
# Maybe this should be multiple scenarios?Document Complex Scenarios
# Multi-party transaction with clear aliases
Scenario: Three-way trade
# Alice sells to Bob, Bob sells to Charlie
Given a user "Alice" exists as {alice}
Given a user "Bob" exists as {bob}
Given a user "Charlie" exists as {charlie}
# ... clear who does what