Skip to content

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:

php
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:

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:

php
// 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:

php
// Instance method execution
$instance = app(UserSteps::class);
$instance->createUser($name);

// Static method execution
User::createWithName($name);  // No instance created

Model Static Methods

The most common use case is with Eloquent Models:

php
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();
    }
}
gherkin
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 draft

Helper Class Methods

Static methods on helper classes are also supported:

php
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, '.', ',');
    }
}
gherkin
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:

php
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:

php
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(),
        ]);
    }
}
gherkin
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 comment

The $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:

php
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]);
    }
}
gherkin
Scenario: Product pricing
  Given a product "Widget" costs 29.99
  Then the product price should be 29.99
php
class 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:

php
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:

php
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 {}
gherkin
Given a user exists    # Creates User instance
Given a post exists    # Creates Post instance

Limitations

No Instance State

Static methods cannot access instance properties:

php
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:

php
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:

php
#[When('payment is processed')]
public static function processPayment(Order $order): void
{
    app(PaymentGateway::class)->charge($order);
}

Best Practices

Use Static for Stateless Operations

php
// 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

php
// 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

php
// 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

php
// 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 { }
}

Released under the MIT License.