Comparison with Behat
If you're familiar with Behat, you'll find many similarities in Pest BDD. However, there are significant differences in philosophy and implementation. This guide helps you understand these differences and migrate existing knowledge.
Feature Comparison
| Feature | Behat | Pest BDD |
|---|---|---|
| Gherkin Support | Full | Full (via behat/gherkin) |
| Configuration | behat.yml required | Zero config |
| Step Binding | @Given annotations | #[Given] attributes |
| Context Classes | FeatureContext required | Any class works |
| Test Runner | Standalone behat command | Integrated with Pest |
| Assertions | PHPUnit assertions | Pest expect() |
| Parallel Execution | Extension required | Built-in (--parallel) |
| Laravel Integration | behat/mink-extension | Native factory support |
| PHP Version | PHP 7.2+ | PHP 8.1+ |
| Languages | 70+ | 70+ (same parser) |
Configuration
Behat
Behat requires a behat.yml configuration file:
# behat.yml
default:
suites:
default:
contexts:
- FeatureContext
paths:
- features
extensions:
Behat\MinkExtension:
base_url: http://localhost
sessions:
default:
goutte: ~Pest BDD
Pest BDD requires no configuration:
# Just run it
pest --bddFeatures are discovered in tests/Behaviors/ and step definitions are found via Composer's classmap.
Step Definitions
Behat
// features/bootstrap/FeatureContext.php
use Behat\Behat\Context\Context;
class FeatureContext implements Context
{
private int $number;
/**
* @Given I have number :n
*/
public function iHaveNumber(int $n): void
{
$this->number = $n;
}
/**
* @When I add :n
*/
public function iAdd(int $n): void
{
$this->number += $n;
}
/**
* @Then the result should be :expected
*/
public function theResultShouldBe(int $expected): void
{
if ($this->number !== $expected) {
throw new \Exception("Expected $expected but got {$this->number}");
}
}
}Pest BDD
// tests/Assertions/CalculatorSteps.php
use TestFlowLabs\PestTestAttributes\Given;
use TestFlowLabs\PestTestAttributes\Then;
use TestFlowLabs\PestTestAttributes\When;
class CalculatorSteps
{
private int $number;
#[Given('I have number {n}')]
public function haveNumber(int $n): void
{
$this->number = $n;
}
#[When('I add {n}')]
public function add(int $n): void
{
$this->number += $n;
}
#[Then('the result should be {expected}')]
public function resultShouldBe(int $expected): void
{
expect($this->number)->toBe($expected);
}
}Key Differences
Annotations vs Attributes: Behat uses docblock annotations (
@Given), Pest BDD uses PHP 8 attributes (#[Given])No Interface Required: Pest BDD classes don't need to implement any interface
Placeholder Syntax: Behat uses
:param, Pest BDD uses{param}Assertions: Pest BDD leverages Pest's
expect()for fluent assertions
Parameter Types
Behat Transformations
// Behat requires explicit transformers
/**
* @Transform :user
*/
public function castUser(string $username): User
{
return User::where('username', $username)->first();
}
/**
* @Given :user is logged in
*/
public function userIsLoggedIn(User $user): void
{
$this->actingAs($user);
}Pest BDD Type Inference
// Pest BDD infers types from method signatures
#[Given('user {name} exists')]
public function userExists(string $name): User
{
return User::factory()->create(['name' => $name]);
}
#[When('the user logs in')]
public function userLogsIn(User $user): void // Auto-injected!
{
$this->actingAs($user);
}Pest BDD automatically:
- Infers regex patterns from type hints
- Stores return values in context
- Injects matching types into subsequent steps
Context Sharing
Behat
// Behat uses constructor injection or shared contexts
class FeatureContext implements Context
{
private SharedContext $shared;
public function __construct(SharedContext $shared)
{
$this->shared = $shared;
}
}Pest BDD
// Pest BDD uses automatic context injection
#[Given('a user {name} exists')]
public function userExists(string $name): User
{
return User::factory()->create(['name' => $name]);
}
#[Then('the user should be active')]
public function userShouldBeActive(User $user): void
{
// $user automatically injected from previous step's return value
expect($user->is_active)->toBeTrue();
}Laravel Integration
Behat
Requires extensions and configuration:
# behat.yml
default:
extensions:
Laracasts\Behat\ServiceContainer\BehatExtension: ~
Behat\MinkExtension:
default_session: laravel
laravel: ~// FeatureContext.php
use Laracasts\Behat\Context\Migrator;
use Laracasts\Behat\Context\DatabaseTransactions;
class FeatureContext implements Context
{
use Migrator, DatabaseTransactions;
}Pest BDD
Native Laravel support with no extra configuration:
// database/factories/UserFactory.php
use TestFlowLabs\PestTestAttributes\Given;
class UserFactory extends Factory
{
#[Given('a user exists')]
public function definition(): array
{
return [
'name' => $this->faker->name(),
'email' => $this->faker->email(),
];
}
#[Given('a user {name} exists')]
public function withName(string $name): static
{
return $this->state(['name' => $name]);
}
#[Given('an admin user exists')]
public function admin(): static
{
return $this->state(['role' => 'admin']);
}
}See Laravel Integration for more details.
Running Tests
Behat
# Run all features
behat
# Run specific feature
behat features/calculator.feature
# Run with tags
behat --tags="@smoke"
# Run specific scenario
behat --name="Addition"Pest BDD
# Run all BDD features
pest --bdd
# Run with filter
pest --bdd --filter="Calculator"
# Run in parallel
pest --bdd --parallel
# Run with tags
pest --bdd --tags="@smoke"Migration Guide
Step 1: Install Pest BDD
composer require testflowlabs/pest-plugin-bdd --devStep 2: Move Feature Files
Move features from features/ to tests/Behaviors/:
mv features/*.feature tests/Behaviors/Step 3: Convert Step Definitions
Convert annotations to attributes and update placeholder syntax:
Before (Behat):
/**
* @Given I have :count items
*/
public function iHaveItems(int $count): voidAfter (Pest BDD):
#[Given('I have {count} items')]
public function haveItems(int $count): voidStep 4: Update Assertions
Replace PHPUnit assertions with Pest expectations:
Before:
assertEquals($expected, $actual);
assertTrue($condition);
assertInstanceOf(User::class, $user);After:
expect($actual)->toBe($expected);
expect($condition)->toBeTrue();
expect($user)->toBeInstanceOf(User::class);Step 5: Remove Behat Configuration
Delete behat.yml and remove Behat dependencies:
composer remove behat/behat behat/mink-extension
rm behat.ymlStep 6: Run Tests
composer dump-autoload --optimize
pest --bddFeature Parity
Available in Both
- Gherkin syntax (Given/When/Then)
- Background steps
- Scenario Outlines with Examples
- Tags and tag filtering
- Multi-language support (70+ languages)
- And/But conjunctions
Pest BDD Advantages
- Zero configuration
- Native PHP 8 attributes
- Automatic type inference
- Built-in context injection
- Laravel factory integration
- Integrated with Pest ecosystem
- Parallel execution built-in
Behat Advantages
- Mature ecosystem with many extensions
- Mink integration for browser testing
- More granular hooks system
- Wider IDE support for annotations