Skip to content

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

FeatureBehatPest BDD
Gherkin SupportFullFull (via behat/gherkin)
Configurationbehat.yml requiredZero config
Step Binding@Given annotations#[Given] attributes
Context ClassesFeatureContext requiredAny class works
Test RunnerStandalone behat commandIntegrated with Pest
AssertionsPHPUnit assertionsPest expect()
Parallel ExecutionExtension requiredBuilt-in (--parallel)
Laravel Integrationbehat/mink-extensionNative factory support
PHP VersionPHP 7.2+PHP 8.1+
Languages70+70+ (same parser)

Configuration

Behat

Behat requires a behat.yml configuration file:

yaml
# 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:

bash
# Just run it
pest --bdd

Features are discovered in tests/Behaviors/ and step definitions are found via Composer's classmap.

Step Definitions

Behat

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

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

  1. Annotations vs Attributes: Behat uses docblock annotations (@Given), Pest BDD uses PHP 8 attributes (#[Given])

  2. No Interface Required: Pest BDD classes don't need to implement any interface

  3. Placeholder Syntax: Behat uses :param, Pest BDD uses {param}

  4. Assertions: Pest BDD leverages Pest's expect() for fluent assertions

Parameter Types

Behat Transformations

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

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

php
// Behat uses constructor injection or shared contexts
class FeatureContext implements Context
{
    private SharedContext $shared;

    public function __construct(SharedContext $shared)
    {
        $this->shared = $shared;
    }
}

Pest BDD

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

yaml
# behat.yml
default:
  extensions:
    Laracasts\Behat\ServiceContainer\BehatExtension: ~
    Behat\MinkExtension:
      default_session: laravel
      laravel: ~
php
// 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:

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

bash
# 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

bash
# 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

bash
composer require testflowlabs/pest-plugin-bdd --dev

Step 2: Move Feature Files

Move features from features/ to tests/Behaviors/:

bash
mv features/*.feature tests/Behaviors/

Step 3: Convert Step Definitions

Convert annotations to attributes and update placeholder syntax:

Before (Behat):

php
/**
 * @Given I have :count items
 */
public function iHaveItems(int $count): void

After (Pest BDD):

php
#[Given('I have {count} items')]
public function haveItems(int $count): void

Step 4: Update Assertions

Replace PHPUnit assertions with Pest expectations:

Before:

php
assertEquals($expected, $actual);
assertTrue($condition);
assertInstanceOf(User::class, $user);

After:

php
expect($actual)->toBe($expected);
expect($condition)->toBeTrue();
expect($user)->toBeInstanceOf(User::class);

Step 5: Remove Behat Configuration

Delete behat.yml and remove Behat dependencies:

bash
composer remove behat/behat behat/mink-extension
rm behat.yml

Step 6: Run Tests

bash
composer dump-autoload --optimize
pest --bdd

Feature 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

Released under the MIT License.