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.