v9.0 — Complete Rewrite

Once you spec,
you never go back

Specification-oriented BDD framework for PHP 8.3+. Describe behaviour. Drive design. Pair with AI. Now phpspec takes agile coding into the AI era.

AI-native by design. Pair program with an LLM that writes specs, generates code, and refactors — all driven by your specifications. Human intent, machine speed.
$ composer require --dev phpspec/phpspec
Terminal
$ vendor/bin/phpspec pair
  PhpSpec Pair Programming Mode

  We're red on App\Calculator: adds two numbers.
  Type /run and I'll offer to create what's missing.
  │ /run
  Looks like you are trying to spec App\Calculator,
  a class that doesn't exist yet.
  Do you want me to create class App\Calculator for you?
   ▸ 1. Yes
     2. Yes, and don't ask again: always create classes
     3. No
  Class App\Calculator generated in src/App/Calculator.php
  │ what's the smallest step now?
  Thinking...
  The example calls add(), which does not exist yet. A stub keeps us honest:
  [MODIFIED] src/App/Calculator.php
     7 +     public function add(int $a, int $b): int
     8 +     {
     9 +     }
  Apply this offered change?
   ▸ 1. Yes
     2. Yes, and don't ask again: always apply offered changes
     3. No

  ~/code/calc      ai: on | provider: anthropic | model: claude-sonnet-5

A fresh start for phpspec

phpspec 9 replaces the ObjectBehavior base class with a closure-based DSL inspired by RSpec and Jasmine. No Prophecy. No magic methods. Just clear, expressive specs.

phpspec 2–7
class CalculatorSpec extends ObjectBehavior
{
    function it_is_initializable()
    {
        $this->shouldHaveType(Calculator::class);
    }

    function it_adds_two_numbers()
    {
        $this->add(2, 3)->shouldReturn(5);
    }
}
phpspec 9
describe(Calculator::class, function () {
    this('calculator', fn() => new Calculator());

    it('is initializable', function () {
        expect($this->calculator)
            ->toBeAnInstanceOf(Calculator::class);
    });

    it('adds two numbers', function () {
        expect($this->calculator->add(2, 3))
            ->toBe(5);
    });
});

Everything you need to spec

One framework. Zero heavy dependencies. Spec BDD and Story BDD unified.

AI

AI Pair Programming

Interactive pair REPL with tool-calling LLMs. next suggests what to spec next. refactor applies behaviour-preserving changes. Anthropic, OpenAI, and Google.

it

Spec BDD

describe, context, it, this, expect: the full closure-based DSL with 30+ built-in matchers, negation, and chaining.

fn

Built-in Mocking

No Prophecy needed. mock(), allow()->toReturn(), toBeCalled()->once(). Argument matchers, type-hinted injection, and fluent call counts.

Gk

Story BDD

Write Gherkin .feature files with given/when/then step definitions. Background, Scenario Outline, DataTable, tags, and shared world.

+ 

Code Generation

Spec-first workflow: describe, exemplify commands. Auto-generate classes, interfaces, method stubs, and step definitions. --fake mode for instant green.

$_

CLI & Reporting

Pretty, Dot, TAP, JUnit formatters. Filter, random ordering, profiling, stop-on-failure, coverage reports, and quiet mode.

⇄

Browser Testing

Built-in HTTP client: visit(), get(), post(). Response matchers for status, JSON paths, headers, and redirects.

++

Extensions

Plug in custom matchers, formatters, commands, and event listeners. Composer-native packages with auto-discovery support.

||

Parallel Execution

--parallel[=N] runs specs across worker processes using Fibers. Result aggregation and ordered output.

Expressive by design

A taste of what specs look like in phpspec 9.

Mocking

$repo = mock(UserRepository::class);
allow($repo->find(1))->toReturn('Alice');
allow($repo->find(2))->toReturn('Bob');

expect($repo->find(1))->toBe('Alice');
expect($repo->find(2))->toBe('Bob');

Story BDD

given('a greeting service', function () {
    $this->greeter = new Greeter();
});

when('I greet {string}', function (string $name) {
    $this->result = $this->greeter->greet($name);
});

then('I should see {string}', function (string $expected) {
    expect($this->result)->toBe($expected);
});

London School — Mock Collaborators, Test in Isolation

use App\UserService;
use App\UserRepository;

describe(UserService::class, function () {
    it('returns the user display name', function (UserRepository $repo) {
        allow($repo->find(1))->toReturn(['name' => 'Alice']);
        $service = new UserService($repo);
        expect($service->getDisplayName(1))->toBe('Alice');
    });

    it('returns Unknown for missing user', function (UserRepository $repo) {
        allow($repo->find(999))->toReturn(null);
        $service = new UserService($repo);
        expect($service->getDisplayName(999))->toBe('Unknown');
    });
});