Writing Specs

Specs are PHP files that describe your objects’ behaviour using describe, context, it, and expect.

Spec file structure

Spec files live in the spec/ directory and use the .spec.php suffix. They mirror the source namespace:

Source classSpec file
App\Calculatorspec/App/Calculator.spec.php
App\Console\Commandspec/App/Console/Command.spec.php

Every spec starts with a describe() call:

<?php
use App\Calculator;

describe(Calculator::class, function () {
    // examples and contexts go here
});

You can also pass a string: describe('Calculator', function () { ... })

The context block

Group related examples by scenario using context:

describe('Stack', function () {
    context('when empty', function () {
        it('has count zero', function () {
            expect([])->toHaveCount(0);
        });
    });

    context('when items are pushed', function () {
        it('has count equal to pushed items', function () {
            expect([1, 2, 3])->toHaveCount(3);
        });
    });
});

context is an alias for describe — they’re functionally identical but by convention describe names a class and context names a scenario.

The it block

Each it block is a single example — one atomic behaviour:

it('adds two numbers', function () {
    $calc = new Calculator();
    expect($calc->add(2, 3))->toBe(5);
});

The let block

let() defines lazy, memoized values that are fresh per example. Access them via $this->name:

describe('SharedState', function () {
    let('items', fn() => [1, 2, 3]);

    it('has access to let values', function () {
        expect($this->items)->toHaveCount(3);
    });

    it('can use let values in assertions', function () {
        expect($this->items)->toContain(2);
    });
});

Type-hinted parameters in let closures auto-inject mocks:

let('notifier', fn(Mailer $mailer) => new Notifier($mailer));

Pending and focused examples

Pending

Mark examples or groups as pending — they’re reported but not executed:

// Pending example — body is NOT executed
xit('is not yet implemented', function () {
    expect(true)->toBe(false); // won't run
});

// Pending at runtime
it('marks itself pending', function () {
    pending('Work in progress');
});

// Pending entire group
xdescribe('PendingGroup', function () { /* ... */ });
xcontext('also pending', function () { /* ... */ });

Focused

Run only focused examples — everything else is skipped:

// Only this example runs
fit('runs this one', function () {
    expect(1)->toBe(1);
});

// Only examples inside this group run
fdescribe('Focused', function () { /* ... */ });
fcontext('also focused', function () { /* ... */ });

Skipping

Skip examples conditionally at runtime:

it('requires Redis', function () {
    skip('Redis not available');
});

Negation with not()

Negate any matcher by chaining not():

expect(5)->not()->toBe(3);
expect('hello')->not()->toBeEmpty();
expect([1, 2])->not()->toContain(99);
expect(42)->not()->toBeNull();

Chained expectations

Chain multiple matchers on the same subject:

expect('hello')
    ->toBeOfType('string')
    ->toStartWith('h')
    ->toEndWith('o');

Error reporting

When a spec fails, phpspec shows expected vs actual values, file and line number, and surrounding code context (with -v):

phpspec run
 shows expected and actual

  Expected "actual" to be "expected"

  at spec/App/ErrorMessages.spec.php:4

If you misspell a matcher, phpspec suggests the closest match:

Unknown matcher "toBeTru". Did you mean toBeTrue?