Static Methods
Pest BDD supports static methods as step definitions. This is particularly useful for Eloquent Model factories, helper methods, and any class where static methods are the natural pattern.
Basic Usage
Add step attributes to static methods just like instance methods:
use TestFlowLabs\PestTestAttributes\Given;
use TestFlowLabs\PestTestAttributes\When;
class User extends Model
{
#[Given('a user {name} exists')]
public static function createWithName(string $name): self
{
return self::create(['name' => $name]);
}
#[When('user {email} is found')]
public static function findByEmail(string $email): ?self
{
return self::where('email', $email)->first();
}
}Usage in Gherkin:
Feature: User Management
Scenario: Create user by name
Given a user "John Doe" exists
Then the user should be in the database
Scenario: Find user by email
Given a user with email "john@example.com" exists
When user "john@example.com" is found
Then the user name should be "John"How It Works
When Pest BDD scans step definitions, it detects static methods:
// During registration
StepDefinition::fromMethod(
type: StepType::Given,
pattern: 'a user {name} exists',
className: User::class,
methodName: 'createWithName',
isStatic: true, // Marked as static
);During execution, static methods are called directly on the class:
// Instance method execution
$instance = app(UserSteps::class);
$instance->createUser($name);
// Static method execution
User::createWithName($name); // No instance createdModel Static Methods
The most common use case is with Eloquent Models:
class Post extends Model
{
#[Given('a published post exists')]
public static function createPublished(): self
{
return self::create([
'title' => fake()->sentence(),
'content' => fake()->paragraphs(3, true),
'published_at' => now(),
]);
}
#[Given('a draft post by {author} exists')]
public static function createDraftByAuthor(string $author, User $user): self
{
// $user is auto-injected from context
return self::create([
'title' => fake()->sentence(),
'user_id' => $user->id,
'author_name' => $author,
'published_at' => null,
]);
}
#[Given('{count} posts exist')]
public static function createMany(int $count): Collection
{
return self::factory()->count($count)->create();
}
}Scenario: Browse published posts
Given a published post exists
And a published post exists
When I view the blog
Then I should see 2 posts
Scenario: Author draft
Given a user "Jane" exists
And a draft post by "Jane Author" exists
When I view Jane's drafts
Then I should see the draftHelper Class Methods
Static methods on helper classes are also supported:
class StringHelper
{
#[When('I slugify {text}')]
public static function slugify(string $text): string
{
return Str::slug($text);
}
#[When('I format currency {amount}')]
public static function formatCurrency(float $amount): string
{
return number_format($amount, 2, '.', ',');
}
}Scenario: Generate slug
When I slugify "Hello World"
Then the result should be "hello-world"
Scenario: Format price
When I format currency 1234.5
Then the result should be "1,234.50"Mixed Static and Instance Methods
A class can have both static and instance methods as steps:
class OrderService
{
private ?Order $lastOrder = null;
// Static method - creates order
#[Given('an order for {amount} exists')]
public static function createOrder(float $amount): Order
{
return Order::create(['total' => $amount]);
}
// Instance method - needs service state
#[When('I process the order')]
public function process(Order $order): void
{
$this->lastOrder = $order;
$order->update(['status' => 'processing']);
}
// Instance method - uses service state
#[Then('the last processed order should be {status}')]
public function assertStatus(string $status): void
{
expect($this->lastOrder->status)->toBe($status);
}
}Static Methods with Auto-Injection
Static methods can receive auto-injected parameters:
class Comment extends Model
{
#[When('a comment is added to the post')]
public static function addToPost(Post $post, User $user): self
{
// Both $post and $user are injected from context
return self::create([
'post_id' => $post->id,
'user_id' => $user->id,
'body' => fake()->paragraph(),
]);
}
}Scenario: Add comment
Given a user "John" exists
And a post "Hello World" exists
When a comment is added to the post
Then the post should have 1 commentThe $post and $user parameters are resolved from the scenario context, just like with instance methods.
Return Value Storage
Return values from static methods are stored in the scenario context:
class Product extends Model
{
#[Given('a product {name} costs {price}')]
public static function createWithPrice(string $name, float $price): self
{
return self::create(['name' => $name, 'price' => $price]);
}
}Scenario: Product pricing
Given a product "Widget" costs 29.99
Then the product price should be 29.99class ProductAssertions
{
#[Then('the product price should be {expected}')]
public function assertPrice(float $expected, Product $product): void
{
// $product injected from the static method's return value
expect($product->price)->toBe($expected);
}
}Factory Pattern
Static factory methods are a common pattern:
class Order extends Model
{
#[Given('a pending order exists')]
public static function createPending(): self
{
return self::factory()->pending()->create();
}
#[Given('a completed order exists')]
public static function createCompleted(): self
{
return self::factory()->completed()->create();
}
#[Given('a cancelled order exists')]
public static function createCancelled(): self
{
return self::factory()->cancelled()->create();
}
}This works well alongside the Factory Integration feature.
Late Static Binding
PHP's late static binding works as expected:
abstract class BaseModel extends Model
{
#[Given('a {type} exists')]
public static function createOne(): static
{
return static::create([]); // Creates the actual subclass
}
}
class User extends BaseModel {}
class Post extends BaseModel {}Given a user exists # Creates User instance
Given a post exists # Creates Post instanceLimitations
No Instance State
Static methods cannot access instance properties:
class OrderService
{
private int $orderCount = 0;
// This WON'T work - static can't access $this
#[Given('an order exists')]
public static function create(): Order
{
$this->orderCount++; // Error: $this not available
return Order::create([]);
}
}No Constructor Injection
Static methods don't benefit from constructor dependency injection:
class OrderService
{
public function __construct(
private PaymentGateway $gateway, // Available to instance methods
) {}
#[When('payment is processed')]
public static function processPayment(Order $order): void
{
$this->gateway->charge($order); // Error: can't access $this->gateway
}
}For static methods that need dependencies, use Laravel's app() helper:
#[When('payment is processed')]
public static function processPayment(Order $order): void
{
app(PaymentGateway::class)->charge($order);
}Best Practices
Use Static for Stateless Operations
// Good: Stateless creation
#[Given('a user exists')]
public static function createUser(): User
{
return User::factory()->create();
}
// Better as instance: Needs accumulated state
#[Then('total count should be {expected}')]
public function assertCount(int $expected): void
{
expect($this->items)->toHaveCount($expected);
}Use Instance for Service Dependencies
// Use instance method when DI is needed
class PaymentSteps
{
public function __construct(
private PaymentGateway $gateway,
private Logger $logger,
) {}
#[When('payment is made')]
public function pay(Order $order): void
{
$this->gateway->charge($order);
$this->logger->info("Charged: {$order->total}");
}
}Return Values for Context Storage
// Good: Returns the created model
#[Given('a product exists')]
public static function create(): Product
{
return Product::factory()->create();
}
// Less useful: No return
#[Given('a product exists')]
public static function create(): void
{
Product::factory()->create(); // Lost - can't inject later
}Prefer Models for Static Steps
// Natural: Static method on Model
class User extends Model
{
#[Given('an admin exists')]
public static function createAdmin(): self { }
}
// Consider instance: Service with dependencies
class UserService
{
public function __construct(private RoleService $roles) {}
#[Given('an admin exists')]
public function createAdmin(): User { }
}