v9.0 — Complete Rewrite

Once you spec,
you never go back

Specification-oriented BDD framework for PHP 8.2+. 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
$ phpspec pair
 
> What's the smallest behavior we want to
  see in our string calculator?
 
How about no argument means zero?
 
+    it('returns zero with no arguments', function () {
+        expect($this->calc->sum())->toBe(0);
+    });
 
Would you like me to create the spec?
1. Yes
2. No
3. Tell me what to do:
 
> 1

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 () {
    let('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, let, 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');
    });
});