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 --bddSmoke 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;
}Minimize Related Models
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 --parallelEnsure 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 statesThis 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 --optimizeAvoid 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" --parallelFail Fast
bash
# Stop on first failure during CI
pest --bdd --stop-on-failureProfiling
Identify Slow Tests
bash
# Show slowest tests
pest --bdd --profileTag 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 completeQuick Wins
| Optimization | Impact | Effort |
|---|---|---|
Use @smoke tags | High | Low |
| Parallel execution | High | Low |
| Factory states | Medium | Low |
| DatabaseTransactions | Medium | Low |
| Minimize relations | Medium | Medium |
| In-memory tests | High | Medium |
| CI job splitting | Medium | Medium |