Writing Specs
PhpSpec uses a Jasmine/RSpec-style DSL built on global functions: describe, context, it, its, this, and expect.
describe(string $title, Closure $examples)
Groups related specs. Typically wraps an entire class under specification. The title is usually the fully-qualified class name.
describe(Calculator::class, function() { // examples go here });
context(string $title, Closure $examples)
Alias for describe. Used to create nested groups that represent specific scenarios or states.
describe(Calculator::class, function() { context("when adding positive numbers", function() { it("returns their sum", fn() => expect(1 + 2)->toBe(3)); }); context("when dividing by zero", function() { // ... }); });
Contexts can be nested to any depth, and the output will indent accordingly.
it(string $title, ?Closure $example = null) / its(...)
Defines a single example (test case). The title should read like a sentence: "it does something".
it("returns the sum of two numbers", function() { $calc = new Calculator(); expect($calc->add(2, 3))->toBe(5); });
its() is an alias for it(), useful for readability:
its("name is John", fn() => expect($this->user->name)->toBe("John"));
Type-Hinted Mock Injection
it() closures support type-hinted parameters that are automatically resolved as mocks:
it('sends notifications', function (Mailer $mailer) { // $mailer is a mock of Mailer, injected automatically expect($mailer)->toBeAnInstanceOf(Mailer::class); });
this(string $property, Closure $setter)
Declares a property on $this for the examples of the block it is in, nested blocks included. The value is built lazily: the closure runs the first time an example reads the property, once for that example, and the example keeps that value. The next example gets a value of its own. A this() no example reads is never built.
describe(Calculator::class, function() { this("calculator", fn() => new Calculator()); it("instantiates", fn() => expect($this->calculator)->toBeAnInstanceOf(Calculator::class)); });
When the example is the first to read it, the beforeEach hooks have run by then: a hook can arrange a double the this() consumes, and a this() may read another declared after it. A closure that throws fails the example that read it, reported as that example's error; the other examples still run.
To build a value before the example starts, because the example relies on what building it does (a server started, a file written), read it in a beforeEach. It is still built once for that example:
this('server', fn () => TestServer::start()); beforeEach(fn () => $this->server);
this() also supports type-hinted mock injection:
this("generator", fn () => mock(SpecGenerator::class)); this("service", fn (UserRepository $repo) => new UserService($repo));
expect(mixed $subject)
Creates an Expectation object to make assertions. See Matchers for all available matchers.
expect($value)->toBe(42); expect($user)->toBeAnInstanceOf(User::class); expect($list)->toHaveCount(3);
When the subject is a mock double (implements MatchableDouble), expect() returns a Mock\Expectation instead, which provides mock-specific matchers like toBeCalled().
Stubbing with allow()
Use allow() to stub mock method return values:
it('returns user data', function (UserRepository $repo) { allow($repo->find(1))->toReturn(['name' => 'Alice']); expect($repo->find(1))->toBe(['name' => 'Alice']); });
See Mocking for full details on stubbing and verification.
Lifecycle Hooks
beforeEach / afterEach
Run before/after every example in the current context:
describe(Calculator::class, function () { beforeEach(function () { $this->calculator = new Calculator(); }); it('adds', fn () => expect($this->calculator->add(2, 3))->toBe(5)); });
A beforeEach that throws fails the example it was preparing, reported as
that example's error; the other examples still run.
beforeAll / afterAll
Run once per context, before/after all examples:
beforeAll(function () { $this->service = ExpensiveService::boot(); });
Hooks inherit to nested contexts. See Hooks for details on execution order and nesting.
Pending and Skipped
An example not written yet is pending: give it a title and no body, cross it out with xit(), or call pending() inside it, with the reason when there is one.
it("applies a discount code"); xit("refunds a cancelled order", function () { // ... }); it("converts the total", function () { pending("Needs the rates API"); // code below is not executed });
xdescribe() and xcontext() leave a whole group pending. A pending example is reported with a ○ (a P under --format dot) and its reason, counted apart from the passes and listed under Pending: at the end. It leaves the run green; --stop-on-pending halts the run at the first one, and so does --stop-on-problems.
An example that cannot run here is skipped: call skip() with the reason.
it("fetches the rates", function () { skip("No network on this machine"); });
A skipped example is reported with a - (an S under --format dot) and its reason, listed under Skipped:, and leaves the run green; --stop-on-skipped halts the run at the first one.
A hook can decide for the examples it wraps. skip() or pending() in beforeEach leaves each example skipped or pending with that reason, and in beforeAll every example of the context, nested ones included; the matching afterEach or afterAll still runs, so what the hook set up is cleaned up:
describe("RatesClient", function () { beforeAll(function () { if (getenv("RATES_API_URL") === false) { skip("No rates API configured"); } }); });
In afterEach or afterAll the example has already run, so skip() or pending() there is an error that says so.
Risky Examples
An example that runs to the end without making a single expectation checked nothing, whatever the code did. It is reported as risky, with a ! (an R under --format dot), counted apart from the passes and listed under Risky: at the end. It leaves the run green: nothing is red, there is only nothing checked.
it("converts the total", function () { $this->converter->convert(100); // risky: no expect() });
Warnings, Deprecations and Notices
A PHP warning, deprecation or notice raised while an example or a step runs is kept on it rather than printed wherever it fell: shown under it as ⚠, ⛔ or ℹ with the line that raised it, listed under Warnings:, Deprecations: or Notices: at the end, and counted on the summary line. It leaves the run green, and --stop-on-warning, --stop-on-deprecation and --stop-on-notice halt the run at the first one.
A deprecation a library raises because your code called something deprecated is reported, whether the library raised it with trigger_error() or trigger_deprecation(). One a library raises for another library's code calling it is left out: nothing you change in your own code makes it go away. A library is anything in a Composer vendor directory or outside the project.
Focused Examples
Run only specific examples or contexts:
fit("only this runs", fn() => expect(true)->toBeTrue()); fdescribe("only this group runs", function() { /* ... */ }); fcontext("only this context runs", function() { /* ... */ });
When any focused block exists in a context, only focused blocks are executed. This is resolved per-context, not globally. The examples left out are reported pending with "left out by focus" as the reason, and the run ends with a warning saying how many were left out. A run with no terminal, as in CI, fails on a focus instead: a fit nobody removed must not pass as the whole suite. Under --format=agent the run does not fail; the summary carries focused, the number left out.
How $this Works
Each spec file is loaded via a Subject object, which is $this inside spec closures. The Subject class has #[AllowDynamicProperties], so a let's value lands on it as a property when an example first reads it. Let values are forgotten before the next example starts, so no example sees another's.
Nesting Structure
Specification (file)
+-- Context (describe/context block)
|-- Example (it block)
|-- Example (it block)
+-- Context (nested context)
+-- Example (it block)
Each describe/context creates a Context object. Each it creates an Example object. Both implement SpecBlock and are composed into a tree via the scope stack.