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 class | Spec file |
|---|---|
App\Calculator | spec/App/Calculator.spec.php |
App\Console\Command | spec/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):
✗ 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?