Skip to content

Performance

Tips for keeping your BDD test suite fast and efficient.

Tag-Based Filtering

Fast Feedback Loop

Use tags to run subsets during development:

gherkin
@fast @unit
Scenario: Validation rules
  ...

@slow @integration
Scenario: Full checkout flow
  ...
bash
# During development - quick feedback
pest --bdd --tags="@fast"

# Before commit - full suite
pest --bdd --tags="not @slow"

# CI pipeline - everything
pest --bdd

Smoke Tests

Mark critical paths for quick verification:

gherkin
@smoke @critical
Scenario: User can login
  ...

@smoke @critical
Scenario: User can checkout
  ...
bash
# Quick health check
pest --bdd --tags="@smoke"

Database Optimization

Use Factory States

Combine states instead of multiple creates:

php
// Efficient: Single create with all attributes
#[Given('a user with complete profile exists')]
public function completeProfile(): static
{
    return $this->state([
        'avatar' => 'path/to/avatar.jpg',
        'bio' => 'About me text',
        'verified_at' => now(),
        'settings' => ['theme' => 'dark'],
    ]);
}

// Inefficient: Multiple database operations
#[Given('a user with complete profile exists')]
public function completeProfile(): User
{
    $user = User::factory()->create();
    $user->update(['avatar' => '...']);
    $user->update(['bio' => '...']);
    $user->verify();
    return $user;
}

Only create what's necessary:

php
// Only create what the test needs
#[Given('a product exists')]
public function product(): Product
{
    return Product::factory()->create();
}

// Don't create unnecessary relations
#[Given('a product exists')]
public function product(): Product
{
    // Avoid: Creates category, brand, reviews, etc.
    return Product::factory()
        ->has(Category::factory())
        ->has(Brand::factory())
        ->has(Review::factory()->count(10))
        ->create();
}

Use In-Memory When Possible

php
// For pure logic tests, avoid database entirely
#[Given('a price calculator')]
public function calculator(): PriceCalculator
{
    return new PriceCalculator();
}

#[When('I calculate price for {quantity} items at ${price}')]
public function calculate(int $quantity, float $price, PriceCalculator $calc): float
{
    return $calc->calculate($quantity, $price);
}

Parallel Execution

Enable Parallel Tests

bash
pest --bdd --parallel

Ensure Independence

For parallel execution to work, scenarios must be independent:

php
// Good: Each scenario gets its own data
#[Given('a user exists')]
public function user(): User
{
    return User::factory()->create();
}

// Bad: Relies on specific ID
#[Given('the user with ID 1 exists')]
public function specificUser(): User
{
    return User::find(1); // May conflict in parallel
}

Database Isolation

Use transactions or separate databases:

php
// In Pest.php
uses(RefreshDatabase::class)->in('Behaviors');

// Or use DatabaseTransactions for speed
uses(DatabaseTransactions::class)->in('Behaviors');

Lazy Evaluation

Factory Chaining

Pest BDD's factory integration uses lazy evaluation:

gherkin
Given a user "John" exists      # Queues state, doesn't create yet
And the user has admin role     # Queues state
And the user has verified email # Queues state
When the user logs in           # NOW creates with all states

This results in a single database insert instead of multiple updates.

Caching

Step Definition Caching

Pest BDD caches step definitions automatically. Ensure the cache is fresh:

bash
# Clear cache during development if steps change
composer dump-autoload --optimize

Avoid Heavy Setup

php
// Avoid: Expensive setup in every scenario
#[Given('the system is ready')]
public function systemReady(): void
{
    $this->artisan('migrate:fresh');
    $this->artisan('db:seed');
    $this->artisan('cache:clear');
}

// Prefer: Use RefreshDatabase trait and minimal seeding
#[Given('a seeded database')]
public function seeded(): void
{
    $this->seed(TestSeeder::class);
}

CI/CD Optimization

Split by Tags

Run different tag groups in parallel CI jobs:

yaml
# .github/workflows/test.yml
jobs:
  smoke:
    runs-on: ubuntu-latest
    steps:
      - run: pest --bdd --tags="@smoke"

  full:
    runs-on: ubuntu-latest
    needs: smoke
    steps:
      - run: pest --bdd --tags="not @smoke" --parallel

Fail Fast

bash
# Stop on first failure during CI
pest --bdd --stop-on-failure

Profiling

Identify Slow Tests

bash
# Show slowest tests
pest --bdd --profile

Tag Slow Tests

gherkin
@slow
Scenario: Generate yearly report
  # This legitimately takes time
  Given a year of transaction data
  When I generate the annual report
  Then the report should be complete

Quick Wins

OptimizationImpactEffort
Use @smoke tagsHighLow
Parallel executionHighLow
Factory statesMediumLow
DatabaseTransactionsMediumLow
Minimize relationsMediumMedium
In-memory testsHighMedium
CI job splittingMediumMedium

Released under the MIT License.