Coding Agents (--format=agent)
PhpSpec speaks a machine-readable dialect designed for coding agents (Claude
Code, Cursor, or any script that shells out and parses output). Instead of the
ANSI-decorated prose the human formatters produce, --format=agent emits JSON
Lines: one self-contained event per line, written as it happens, where the
failure itself is the payload: everything an agent needs to decide its next
move, without re-reading the project.
The same idea runs through the scaffolding commands: describe --format=agent and
exemplify --format=agent return JSON receipts instead of prose, and --accept-offers
applies PhpSpec's code-generation offers non-interactively. Together they let an
agent drive the whole spec → red → green loop through the CLI, never guessing at
output and never hand-editing what PhpSpec can generate.
Just want the CLAUDE.md snippet? Jump to Teaching an agent to use PhpSpec.
Running
$ vendor/bin/phpspec run --format=agent
Standard output carries one JSON object per line, with no ANSI and no prose. Decode a line, act on it, decode the next: on a long suite the first failure reaches you while the rest is still running.
It is --parallel-safe: a worker reports every result back over PhpSpec's own
wire, so an entry from a parallel run carries the same detail as one from a
single process, expectation, output, spec and rerun included. Entries
arrive in completion order rather than file order.
The stream
A run of three examples (one passing, one failing, one erroring on a missing class) produces four lines:
{"v":2,"event":"run_started","suite":"default","seed":null,"php":"8.3.16","coverage":false,"guard":"off"}
{"v":2,"event":"example","id":"6fd046add251","example":"App\\Basket > totals the prices of its products","state":"failing","expectation":{"matcher":"toBe","expected":4000,"actual":3500,"negated":false},"message":"Expected 3500 to be 4000","spec":"spec/App/Basket.spec.php:6","rerun":"run spec/App/Basket.spec.php:6"}
{"v":2,"event":"example","id":"66b1647a77b6","example":"App\\Basket > applies a coupon","state":"error","message":"Class \"App\\Coupon\" not found","exception":{"class":"Error","message":"Class \"App\\Coupon\" not found","at":"spec/App/Basket.spec.php:8"},"spec":"spec/App/Basket.spec.php:8","rerun":"run spec/App/Basket.spec.php:8","offer":{"action":"create_class","target":"App\\Coupon"}}
{"v":2,"event":"summary","examples":3,"scenarios":0,"steps":0,"passing":1,"failing":1,"errors":1,"pending":0,"skipped":0,"risky":0,"warnings":0,"deprecations":0,"notices":0,"actionable":2,"duration_ms":4,"offers":[{"action":"create_class","target":"App\\Coupon"},{"action":"fake_method","target":"App\\Basket::total","value":"4000"}]}
Every line carries "v", the agent-protocol version (currently 2; version
1 was a single JSON object), and an "event" naming its kind:
event | When | Meaning |
|---|---|---|
run_started | always, first | The run began: what it targets and the seed it was shuffled with. |
example | per entry | One example or scenario that needs attention, the moment it is known. |
fatal | when the run died | What stopped it; the counts describe only what ran before it. |
summary | always, last | The totals, and the one number to branch on. |
The order is guaranteed: run_started first, summary last, whatever happened
in between. A run that never started still emits both.
run_started: the header
suite is what the run targets, as the paths were given; seed is the
random-order seed when one was used, else null. php is the version that
ran, coverage says whether coverage is being collected, so a coverage
verdict will follow, and guard is on or off; a guard that is on with no
coverage driver to judge with stops the run instead, as a fatal with the
remedy. The totals are not here: at this point nothing has run yet, and the summary
carries them.
example: only what needs attention
Passing entries are omitted unless you ask for them. A green suite of
thousands need not spend tokens on entries an agent will never act on; the
summary still counts them. Each listed entry is one that failed, errored, or is
pending/skipped. Under -v every passing example and scenario is reported too,
as {id, example, state: "passing", spec, rerun}, so you can name the examples
that cover a change and re-run one of them on its own. The summary is the same
either way, and its rerun still names only what failed.
One entry is one thing to fix. A spec run reports examples; a Story BDD run
reports scenarios, not steps, because a scenario is what fails, what re-runs,
and what you act on. The steps that did not pass ride inside the entry, so one
broken scenario is a single item naming the step that broke it, never a failure
followed by a train of skipped siblings pointing at the same line. Each row of a
Scenario Outline is its own entry, named by its values
(Adding > Adding numbers (1, 2)).
| Field | Meaning |
|---|---|
id | A stable identifier for this example: a hash of the file it lives in and its full name. It survives edits that move lines or change where a failure fires, so you can ask "is THIS exact failure still here?" across runs, and it tells apart examples that share every title in different files. Recomputable from the file in spec and example. |
example | The full name, as a path through every describe and context to the example, which is what --filter selects by: App\Basket > when empty > totals the prices for a spec, Checkout > Paying for a basket for a scenario. A title a sibling before it already has is numbered, totals the prices #2, as every formatter names it. |
state | failing, error, pending, skipped or risky (ran without making an expectation); passing when it raised a PHP warning, deprecation or notice, and otherwise only under -v. |
message | What went wrong, whatever the state; for pending and skipped, the reason the example gave, when it gave one. An error entry keeps exception too, for the class and the site. |
spec | The line to act on, project-relative and always in the spec file: the failing expect(), or the line where an error surfaced in the spec. An error thrown inside the code under test, or an expectation asserted in a helper, is addressed by the it() line that reached it; exception.at keeps the throw site. For a passing, pending or skipped example it is the it() line; for a scenario, the line its Scenario: keyword sits on. Absent when the site is not known. |
rerun | The exact arguments to re-run just this one example or scenario: prepend your PhpSpec binary. It targets the it() line that declares the example (the Scenario: line for a scenario), which PhpSpec resolves to that one and no other. An it() in a loop declares several examples on one line, so each of those is targeted by its file and id instead: run spec/App/Calc.spec.php@6fd046add251. No full-suite re-run needed to verify one fix. It carries the --config the run was named and the --bootstrap it was given, so it runs against the same project; a config PhpSpec found by itself is found again from the same directory. An argument a shell would split or expand is single-quoted. Absent with spec. |
rerun_argv | The same re-run as an argument list, ["run", "spec/App/Basket.spec.php:6", "--format=agent"] followed by the same --config and --bootstrap, to hand your PhpSpec binary and a process API: no quoting, and the stream stays JSON Lines. Absent with rerun. |
output | What the code printed while this entry ran, present only when it printed something. See Printed output. |
attachments | Context the spec or scenario handed over about itself, by name. See Handing over context. |
steps | Scenarios only: the steps that did not pass or that raised a PHP note, each { title, state, message?, expectation?, at?, warnings?, deprecations?, notices? }, in the order they were declared. |
failing entries also carry expectation, both sides in one block:
"expectation": {"matcher": "toBeTrue", "expected": true, "actual": false, "negated": false}
-
matcher: the matcher's name (toBe,toThrow, …),nullfor a custom or predicate matcher, which reaches PhpSpec anonymously. -
expectedis what the matcher wanted,actualwhat your code produced, in the universal sense of those words. (PhpSpec's internal naming is the reverse; the formatter un-inverts it for you.) negated:truewhen the expectation usednot().-
diff: present when both sides are strings, or arrays of one shape, that differ: for strings{ "offset", "expected", "actual" }, the byte where they part and a window of each from there; for arrays{ "path", "expected", "actual" }, the key where they part (items[2].price). A trailing newline stops costing a re-read.
A matcher that names its target only in prose still states it as a value, so
toBeTrue() reports expected: true rather than the null it has no argument
for. Emptiness wants the empty form of what it was given: "" against a string,
[] against an array, 0 against a number. A matcher with no target at all,
such as a predicate, says "N/A" rather than reporting a null one.
toThrow compares what was asked for against what happened, not against the
callable you handed it:
| Expectation | expected | actual |
|---|---|---|
toThrow() and it threw | "N/A" | "Exception" |
toThrow() and it did not | "N/A" | "No exception" |
toThrow(RuntimeException::class) | "RuntimeException" | "RuntimeException" |
toThrow(RuntimeException::class, 'boom') | "RuntimeException(\"boom\")" | "RuntimeException(\"boom\")" |
toThrow(new RuntimeException('boom')) | "RuntimeException(\"boom\")" | "RuntimeException(\"boom\")" |
toThrow(RuntimeException::class) and it did not | "RuntimeException" | "No exception" |
toThrow() with no argument means "throw something".
A mock expectation (toBeCalled, toHaveBeenCalled) compares
the call the spec wanted with the calls the double received, arguments included:
"expectation": {"matcher": "toBeCalled", "expected": {"method": "App\\Ledger::record", "arguments": ["sale"], "times": "at least 1"}, "actual": {"method": "App\\Ledger::record", "calls": [{"arguments": ["refund"]}]}, "negated": false}
arguments are the ones the expectation was written with, [] for a call
written with none, and an argument matcher reads as it was written: any(),
cetera(), type(string), anInstanceOf(App\Task).
A story step that failed an expectation reports the same block, on the step
inside steps (with at, the file:line of the expectation in your step file)
and hoisted onto the entry next to message, so acting on the entry never means
going a level deeper. A step whose code threw has a message and no
expectation: there was none to report.
error entries carry exception (class, message, at) as well. When
the error names something PhpSpec can generate (a missing class, interface, or
method), the entry also carries a per-example offer (see below). A failing
expectation carries no offer: the code exists and its behaviour is simply
wrong, so state: failing already says everything.
Warnings, deprecations and notices. A PHP warning, deprecation or notice
raised while an example or a step ran is listed under warnings,
deprecations or notices: an array of { "message", "at" }, where at is
the line that raised it, in the spec, the steps file or the code under test.
An example carries its own on its entry; a scenario carries each on the step
that raised it. An example or scenario that passed but raised one is reported
all the same, state: passing with its spec and rerun, so a note never goes
by unseen. The keys are present only when non-empty. Sometimes the deprecation
is the actual clue behind a failure. A deprecation a library raised for another
library's code calling it is left out, as it is on the console (see
Warnings, deprecations and notices).
{"v":2,"event":"example","id":"4c2e34fc4bc4","example":"App\\Ledger > totals the entries","state":"passing","spec":"spec/App/Ledger.spec.php:3","rerun":"run spec/App/Ledger.spec.php:3","warnings":[{"message":"totals are rounded","at":"src/App/Ledger.php:9"}]}
{"v":2,"event":"example","id":"ffd3b79b2ddb","example":"Counting > Counting up","state":"passing","steps":[{"title":"Given I run the counter","state":"passing","deprecations":[{"message":"run() is deprecated, use count()","at":"features/steps/steps.php:4"}]}],"spec":"features/counting.feature:2","rerun":"run features/counting.feature:2"}
Large or object values in expectation are exported compactly (long
strings and arrays are truncated with a { "truncated": true, "length": N }
marker) so one entry can never flood a context window. Objects are named by what
they are, not by which instance they were: an enum as App\Status::Active,
anything that can describe itself as App\Money("12.00 GBP"), everything else
as App\Basket.
Two runs of the same failure therefore report the same value. Floats keep their
fraction, so 90.0 is never reported as the 90 it was compared against. A
float JSON has no word for is named instead: "INF", "-INF", "NAN", at any
depth; a byte that is not UTF-8, in a value or in printed output, is replaced
by U+FFFD. An event is never reduced to an empty object: should anything in it
still defeat the encoder, the event keeps its identity and says what was
dropped in encoding.
Printed output
Anything your code prints while an entry runs (an echo left in a spec, a
var_dump, the output of a process a step shelled out to and echoed) is
captured and reported as that entry's output, rather than landing in the
middle of the stream and breaking it. What beforeEach and afterEach print
belongs to the example they wrap and rides in the same output, before and
after what the body printed. For a scenario it is everything its steps
printed, in order, including the steps that passed: a scenario usually runs
the process in one step and reads the result in another. Anything printed
outside an example altogether, from a describe body or a beforeAll, goes to
standard error, so standard output stays the document and nothing else.
output is a string, or { "truncated": true, "length": N, "value": "…" } when
there was more than 4000 characters of it. The key is absent when nothing was
printed.
Handing over context PhpSpec cannot see
Some of what explains a failure never passes through PhpSpec: a log file a
process is writing, the output a step captured into a variable, the body of an
HTTP response. Hand it over with attach(), and it is reported as the entry's
attachments if the run needs attention:
use function PhpSpec\attach; given('the watcher is running', function () { $this->log = $this->workspace . '/watch.log'; $this->process = proc_open($cmd, [1 => ['file', $this->log, 'w']], $pipes); attach('watch log', fn() => file_get_contents($this->log)); });
"attachments": { "watch log": "── SYNTAX ERROR ── src/Todo.phunkie:1:9 ──\nSyntax error, unexpected ';'" }
Four rules make that work, and each one exists because of a way it would otherwise quietly fail:
-
A closure is read at the end, not when you attach it. The log above is
empty at
proc_opentime; what you get is everything the process wrote before the failure. Pass a string instead when the value is already final:attach('stdout', $process->getOutput()). -
Read before your teardown runs. An
afterScenariothat terminates the process and deletes the workspace cannot empty the attachment first. - Nothing is read when the run is green. A closure over a huge log costs nothing on a passing suite, so attach freely.
- The same name twice keeps the latest. A helper attaching on every poll leaves one attachment, not a pile.
Attach inside your helpers, not at every call site. A helper is usually the only thing that knows why it gave up, and returning a bool throws that away:
$eventually = function (callable $condition, float $seconds = 10.0) { // ... poll until $condition holds or the time runs out ... attach('waited', sprintf('%.1f seconds, and the condition never held.', $seconds)); return false; };
Values follow the same cap as output: a string, or a truncation marker past
4000 characters. A closure that throws, or that returns false (what
file_get_contents answers when it cannot read), is reported as
{ "error": "…" } rather than as an empty attachment, so "PhpSpec could not
look" never reads as "the subject said nothing".
Browser specs get this for free. Every request through the browser DSL
attaches http.request (the method, URL and status) and http.response (the
body), so an assertion that says expected 200, actual 500 is read next to what
the server actually said.
summary
The counts (passing, failing, errors, pending, skipped, risky) are for the
whole run, in the units the entries are reported in: one per example, one per
scenario. steps is a size, not a verdict. warnings, deprecations and
notices count every PHP note raised, by examples and steps alike, each one
listed where it was raised. Like risky, they are never actionable: the run
passed, and a deprecation raised in a vendor's code may be nothing the project
can fix. The one number to branch on is
actionable = failing + errors + pending, plus a coverage gate the run
missed and anything that stopped it.
Zero means there is nothing to do. It counts more than the exit code
does: a pending example is work left to do, but, like PHPUnit's incomplete
tests, it leaves the exit code at 0.
duration_ms is the run's wall time.
| Field | Present when | Meaning |
|---|---|---|
rerun | an entry needing attention has a location | One command that re-runs every failing example, and every one that raised a note, at once, so a fix is checked against all of what it was meant to fix. It carries the run's --config and --bootstrap as an entry's rerun does. |
rerun_argv | with rerun | The same as an argument list, carrying --format=agent. |
coverage | a --coverage* option was given | { "percent", "required", "met" }. required is null without --coverage-min, and met is then always true. A missed gate adds 1 to actionable. |
guard | guard is on and either judged the change or could not | { "held": false, "judged": true, "violations": [{ "file", "lines", "member", "remedy" }] }. Each violation is new logic no example reaches, and adds 1 to actionable. When judged is false there are no violations and a reason says what stopped it. |
offers | the run found code it can generate | The run-wide, de-duplicated list. Absent when there is nothing to take. |
stopped_by | a stop option cut the run short | The option that did, --stop-on-warning for one: what came after it did not run, so the counts are not the suite's. |
focused | the spec focused with fit or fdescribe | How many examples were left out, pending. This run is not the suite; the exit code does not say so to an agent, this does. |
applied | --accept-offers was asked to write | { "offers": [{ "id", "action", "target", "file", "applied", "reason"? }], "files", "verified": false }: what was written after the run, under the ids the offers carried, and what could not be, with applied: false and the reason. files names only what changed. The counts describe the code before it, so run again to verify. |
fatal: when the run could not finish
A run that never started (a missing bootstrap, an unknown format, a config that
cannot be taken, an option run does not take or one missing its value), that
was narrowed by --filter, --tags or paths to nothing at all, or that died
partway (a parse error, a class that fails to compile) still answers. It emits a
fatal line of { "message", "at", "remedy"? }, keeps whatever it managed to
collect, and counts 1 in actionable. remedy is present when the way past is
known: a run that wanted coverage and found no engine on gets the same command
under the engine it has, php -d pcov.enabled=1 or XDEBUG_MODE=coverage, a
missing bootstrap or path gets what to point
where, a --filter that matches nothing is told it is not a pattern, and a
file:LINE selector that reaches no example is told to point
inside an it() or a Scenario:
{"v":2,"event":"run_started","suite":"default","seed":null,"php":"8.3.16","coverage":false,"guard":"off"}
{"v":2,"event":"fatal","message":"Class Mute contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (Speaks::speak)","at":"spec/App/Broken.spec.php:8"}
{"v":2,"event":"summary","examples":0,"actionable":1,"…":"…"}
Standard output carries the stream, and nothing else
Under --format=agent those lines are the only thing written to standard
output: the randomised-order seed, the --profile table, coverage lines, report
notes, error text and anything your own code printed all go elsewhere, and PHP's
own fatal reports are sent to the error stream. Parse stdout a line at a time;
read stderr when you want the human text as well.
Offers: code PhpSpec can generate for you
An offer is a { action, target } pair (with an optional value). They are
the same "shall I create this?" prompts the interactive runner shows a human,
turned into flat data:
action | target | Meaning |
|---|---|---|
create_class | App\Coupon | A referenced class doesn't exist yet. |
create_interface | App\Repository | A mocked type doesn't exist yet. |
create_method | App\Basket::checkout | A called method doesn't exist on its class/interface. |
create_steps | features/steps/steps.php | The run has undefined steps; accepting appends them all to that file. |
fake_method | App\Basket::total | An existing empty method that a spec pins to a value; value is the return expression --fake would insert (e.g. "4000", "'hello'", "true"). |
Offers appear in two places: run-wide on the summary's offers, and per-example
on the offer field of an error entry that maps to a concrete generation.
Acting on offers
PhpSpec never applies a change because nobody was there to say no. Every
offer carries an id and waits under it, so you accept what you have actually
read, in a second command:
$ vendor/bin/phpspec accept o_7f3a1c2d # apply exactly that offer $ vendor/bin/phpspec accept o_7f3a1c2d o_91b0e4aa # several at once, all or nothing
An id is derived from what the offer would do, so the same offer keeps it
between runs and a new one is visibly new. accept refuses an id it has never
seen, and refuses an offer whose file has changed since it was made: at that
point the decision was taken about something else. Nothing is applied unless
every named offer can be.
With --format=agent, accept answers with one receipt:
{"v":2,"action":"accept","accepted":[{"id":"o_7f3a1c2d","action":"create_class","target":"App\\Coupon","files":["src/App/Coupon.php"],"applied":true}]}
target is what the offer named, a class, a method, a feature file, the
path of a proposed write, or the class a plan refactors; files are the
files it wrote, from the project root. An offer that could not be applied
(a method written by hand meanwhile, a class file not where the layout says,
a plan that stopped short) comes back with applied: false and a reason,
and the command exits 1. Generated classes land where the project's layout says their namespace
lives (see Code Generation).
For the common case of taking everything a run found, the bulk shortcut remains:
$ vendor/bin/phpspec run --accept-offers # create missing classes/interfaces/methods/steps $ vendor/bin/phpspec run --accept-offers --fake # ...and fill empty methods with their spec'd return values
--accept-offers runs the suite, applies all of that run's offers (no prompts),
and exits 0. Add --fake to also fill empty method bodies with the hardcoded
returns their specs expect (the fake_method offers), a fast way to a first
green before you replace the fakes with real logic.
The summary of such a run says what was written, in applied: each piece
under the id its offer carried, the files it changed, and verified: false,
because the counts on the same line describe the code before the writing.
Run again to verify; the exit code of 0 says the offers were applied, not
that the suite is green.
generate proposes rather than writes: its receipt carries an id per
proposal with applied: false, and accept writes exactly the content that was
reported.
Scaffolding with JSON receipts
describe and exemplify take --format=agent, which swaps their prose for a
one-line JSON receipt, so an agent can scaffold without parsing English. No file
content is returned; the agent reads the file itself.
$ vendor/bin/phpspec describe App/Basket --format=agent
{"v":2,"action":"describe","class":"App\\Basket","spec":"spec/App/Basket.spec.php","created":true}
A command line the command cannot read (an option it does not take, an argument
too many, a command that does not exist) is refused the same way, as
{ "v", "action", "error" } with exit code 1, and nothing else on standard
output. A name under none of the mapped namespaces, with no default_namespace
to put it under, is refused with the remedy, exit code 1:
{"v":2,"action":"describe","error":"Acme\\Thing is under none of the mapped namespaces: Brew\\.","remedy":"Describe Brew\\Acme\\Thing, name a default_namespace in the phpspec config to put every such name under, or answer when asked which one."}
created is false when the spec already existed (both commands are
idempotent). Combine describe --format=agent with -e to also add a method example:
$ vendor/bin/phpspec describe App/Basket --format=agent -e checkout
{"v":2,"action":"describe","class":"App\\Basket","spec":"spec/App/Basket.spec.php","created":false,"example":{"method":"checkout","added":true}}
exemplify --format=agent adds a single method example to a spec (creating the spec
first if needed):
$ vendor/bin/phpspec exemplify App/Basket checkout --format=agent
{"v":2,"action":"exemplify","class":"App\\Basket","method":"checkout","spec":"spec/App/Basket.spec.php","added":true}
added is false when an example for that method is already present.
Refactoring
refactor --format=agent answers with the plan as one receipt, offered once
and whole. Nothing is written until you accept it, which carries every step
out as refactor --no-interaction does (see How it works).
$ vendor/bin/phpspec refactor Brew/DrinkPricer --format=agent
{"v":2,"action":"refactor","class":"Brew\\DrinkPricer","source":"src/Brew/DrinkPricer.php","spec":"spec/Brew/DrinkPricer.spec.php","technique":"Introduce Parameter Object","rationale":"priceOf takes size, milk and shots, which always travel together; a Drink holds them.","steps":["Describe Drink","Implement Drink","Add price method to DrinkPricer","Update DrinkPricer specs to use price","Remove priceOf method"],"offer":{"id":"o_4e6db6b4","action":"refactor","target":"Brew\\DrinkPricer"}}
$ vendor/bin/phpspec accept o_4e6db6b4 --format=agent
{"v":2,"action":"accept","accepted":[{"id":"o_4e6db6b4","action":"refactor","target":"Brew\\DrinkPricer","files":["spec/Brew/Drink.spec.php","src/Brew/Drink.php","src/Brew/DrinkPricer.php","spec/Brew/DrinkPricer.spec.php"],"applied":true}]}
class, source and spec say what was resolved; method appears when the
target named one. When the model finds nothing worth changing, the receipt
carries its reason as declined and no offer. Both exit 0.
files in the accept receipt are what the kept steps wrote. A plan that
stopped short comes back applied: false with the reason, and accept
exits 1. A plan made reading a file that has changed since is refused like
any stale offer.
A refusal carries error, a remedy when there is one, and failures when
the specs are not green; it exits 1:
{"v":2,"action":"refactor","error":"Specs must pass before refactoring.","remedy":"Fix what the run left first.","failures":["spec/App/Broken.spec.php:6 Broken > fails: Expected true to be false"]}
The loop
Putting it together, an agent drives the full cycle through the CLI:
-
Scaffold:
describe App/Basket --format=agent(andexemplify … --format=agentper method) to lay down specs. -
Read:
run --format=agent; act on eachexampleline as it arrives, and branch on thesummaryline'sactionable. -
Generate: for
create_*offers, either write the code or runrun --accept-offers; forfake_methodoffers,run --accept-offers --faketo get a first green. -
Fix & verify: implement real behaviour for
failingentries, thenrun <entry.rerun>to check just that one example. Track it byidacross runs. - Repeat until
actionableis0(exit code0).
Teaching an agent to use PhpSpec
Drop something like this into your CLAUDE.md (or .cursorrules, or any
agent-instruction file) so the agent uses PhpSpec effectively:
## Running specs with PhpSpec
Always run PhpSpec with the machine-readable formatter and parse the JSON:
vendor/bin/phpspec run --format=agent
It prints JSON Lines: one JSON object per line. Decode each line and branch on
its `event`:
- `summary` is the last line. Its `actionable` is the number to act on. **0 means
there is nothing left, so stop.** It counts `failing + errors + pending`, plus
a missed `--coverage-min` gate and anything that stopped the run.
- A `fatal` line means the run could not finish. Read it before anything else:
the counts describe only what ran before it.
- `guard` on the summary means you wrote logic no example reaches. Each
violation names the `member` and the `remedy`: write that example, do not
weaken the code to get past it. If `judged` is `false`, guard reached no
conclusion at all and `reason` says why: nothing was checked, so do not read
the run as having been guarded.
- `example` lines are what needs attention (a passing example is reported only
when it raised a PHP note, or under `-v`). Each has a `state`:
- `failing`: the code ran but behaviour is wrong. Look at
`expectation.expected` (what the spec wants), `expectation.actual` (what the
code produced), and `message`.
Fix the implementation; do not change the spec to match the code unless the
spec is genuinely wrong.
- `error`: an exception was thrown. `message` says what, `exception` adds the
class and the site. If the entry has an `offer`, PhpSpec can generate the
missing piece.
- `pending`: an unimplemented example; implement it.
- `risky`: an example that made no expectation, so it checked nothing;
give it one. Not actionable: nothing is red.
- `warnings`, `deprecations` or `notices` on an entry, or on one of its
`steps`, are what PHP raised while it ran, at `at`. Fix the ones raised in
this project's code. They never count in `actionable`, so read the summary's
`warnings`, `deprecations` and `notices` before you stop.
- `output` on an entry is what the code printed while it ran: read it, it is
often the whole diagnosis for a scenario that drove a process of its own.
- `attachments` on an entry is context the spec handed over about itself, by
name: a log, a captured stdout, an HTTP response body. Read it before you go
looking at files yourself.
- To verify a single fix, re-run just that example: take its `rerun` value and
prepend the binary, e.g. `vendor/bin/phpspec run spec/App/Basket.spec.php:6`. Don't
re-run the whole suite to check one change. The `summary`'s `rerun` does the
same for every failure at once.
- Track a specific failure across runs by its `id` (stable across edits that
move lines). A failure is fixed when its `id` no longer appears. A failing
Story BDD scenario has an `id` and a `rerun` of its own, just like an example.
## Learning the spec-writing API
Before writing specs, read the surface off the code once instead of guessing
or reflecting on the source:
vendor/bin/phpspec api --format=agent
One JSON object: every DSL function and matcher with its signature and
summary, the mock and story functions, and the `timing` rules. Read `timing`
first: an expectation is judged on the line it is written, `toThrow()` runs its
callable there, and only `toBeCalled()` waits for the end of the example.
## Writing specs that explain themselves
When a spec or a step knows something PhpSpec cannot see, hand it over. Without
this, an assertion on a log file or a subprocess reports `Expected false to be
true` and nothing else, and you spend a cycle finding out why:
use function PhpSpec\attach;
attach('stdout', $process->getOutput()); // a value you already hold
attach('watch log', fn() => file_get_contents($f)); // read at failure time
A closure is read at the end of the example or scenario and before any teardown,
so it captures what the process wrote *after* you attached it and survives an
`afterScenario` that deletes the workspace. Nothing is read when the run passes.
Attach inside a polling or process helper rather than at each call site: the
helper is the only thing that knows why it gave up.
## Generating code
Prefer letting PhpSpec generate boilerplate over writing it by hand:
- Scaffold a spec: `vendor/bin/phpspec describe App/Basket --format=agent`
- Add a method example: `vendor/bin/phpspec exemplify App/Basket checkout --format=agent`
- Apply all pending offers (missing classes/interfaces/methods/steps):
`vendor/bin/phpspec run --accept-offers`
- Also fill empty methods with their spec'd return values (fast first green):
`vendor/bin/phpspec run --accept-offers --fake`
- The summary of an `--accept-offers` run carries `applied`: what was written,
by file. Its counts describe the run before the writing, so run again to
verify; exit code 0 means applied, not green.
`--fake` produces hardcoded returns to reach green quickly; always replace them
with real logic before considering the work done. Offers are suggestions, not
commands: skip any that don't fit the design.
## Workflow
1. `describe`/`exemplify` to lay down specs (red).
2. `run --format=agent`; if `actionable > 0`, act on the entries.
3. Use `--accept-offers` for missing code; implement real behaviour for
`failing` entries.
4. Re-run each fixed example via its `rerun`; repeat until `actionable` is 0.
Exit codes
Unchanged from every other formatter: 1 when an example or scenario fails or
errors, when a coverage gate is missed, or when guard finds untested logic;
0 otherwise, pending examples included. --accept-offers exits 0 after
applying offers.
See also
- CLI Reference: every
runoption, and the human formatters. - Code Generation: how the offers are produced.
- Story BDD: feature runs (
steps) and step generation.