Darkwood · PHP · PHPUnit · agents · Flow
Opening
Not because PHPUnit is weak.
Because the oracle chose implementation over meaning.
Fabien Potencier · Sebastian Bergmann
Weak tests under agent velocity
What decides “correct”
Scope
Jobs = behavior · Drivers = furniture
The economics flipped
Cost of writing
≫ cost of choosing oracles
Cost of writing
≪ cost of choosing oracles
Volume without oracles is false insurance.
Scarce skill
Vocabulary
while leaving business meaning unasserted.
Alive in CI. Dead as insurance.
Bad · 01 · ImplementationCoupledTest
$parse->expects($this->once())
->method('__invoke')
->with($raw)
->willReturn($parsed);
// …
$this->assertTrue(true); // Green. Never asserted meaning.
Broken real ParseJob → suite stays green.
Bad · 02 · RuntimeCoupledTest
public function await(array &$stream): void
{
++$this->counter->awaitCalls;
$this->inner->await($stream);
}
$this->assertSame(1, $counter->awaitCalls);
Correct excerpt + driver swap → still red. Wrong claim.
Bad · 03 · CoverageTheaterTest
$this->assertInstanceOf(ExcerptResult::class, $result);
$this->assertNotNull($result->title);
$this->assertIsString($result->excerpt);
// Never asked: is the excerpt faithful?
Coverage theatre. Agents love it.
Same workflow · three lies
Mocks erase Jobs
Pins the Driver
Green on nonsense
What should we assert instead?
Flow · Jobs vs Drivers
flowchart TD
Req[DocumentRequest] --> Fetch[FetchJob]
Fetch --> Parse[ParseJob]
Parse --> Excerpt[ExcerptJob]
Excerpt --> Validate[ValidateJob]
Validate --> Result[ExcerptResult]
subgraph behavior ["Business behavior — test this"]
Fetch
Parse
Excerpt
Validate
end
subgraph furniture ["Runtime furniture — do not pin"]
D1[FiberDriver]
D2[AmpDriver]
D3[ReactDriver]
end
behavior -.->|scheduled by| furniture
Workflow · not furniture
yield new FetchJob($source);
yield new ParseJob();
yield new ExcerptJob($maxExcerptLength);
yield new ValidateJob($minExcerptLength);
Same chain. Inject FiberDriver or AmpDriver. Jobs do not care.
Good · JobBehaviorTest
$parsed = (new ParseJob())(new RawDocument('doc://1', $html));
$this->assertSame('Hello Flow', $parsed->title);
$this->assertSame(
'Jobs transform packets. Drivers schedule them.',
$parsed->body,
);
No Flow. No Driver. Still business.
Good · WorkflowBehaviorTest
$result = FlowCollector::run($flow, new Ip(
new DocumentRequest('fixture://article'),
));
$this->assertSame('Darkwood Flow Notes', $result->title);
$this->assertStringContainsString(
'Jobs transform information packets',
$result->excerpt,
);
Oracle = excerpt meaning. Not call order.
Good · MultiDriverBehaviorTest
flowchart LR
Oracle[Same behavioral oracle] --> Fiber[FiberDriver]
Oracle --> Amp[AmpDriver]
Fiber --> Green1[Green]
Amp --> Green2[Green]
Bad[await / driver spies] --> Fiber
Bad --> Red[Red after swap]
yield 'fiber' => [new FiberDriver()];
yield 'amp' => [new AmpDriver()];
// identical assertions on title + excerpt
Survive the driver swap → you tested the product.
Beyond Flow
Call stack ≠ outcome
Promises / loop furniture
Amp types ≠ domain packets
HTTP adapter ≠ product
If the suite breaks while product meaning is unchanged — you tested furniture.
Doubles under agents
InMemoryDocumentSource
Unit-test App\Job\*composer test:good
Mock DriverInterface
Assert job call order
Treat Bad as insurance
$source = new InMemoryDocumentSource([
'fixture://article' => $html,
]);
Guillaume Moigneu · Agent Skills
Use $pressure-test-testing-decisions on
tests/Bad/ImplementationCoupledTest.php —
should we keep, rewrite, or delete it?
Useful grill · one at a time
One habit change
“Add unit tests.
Aim for high coverage.”
“Pressure-test first.
Oracle on Job / workflow meaning.
Stub the port. No driver spies.”
flowchart TD
Ask[Agent proposes a test] --> PT{Pressure-test}
PT -->|furniture / theatre| Del[Delete or park under Bad]
PT -->|Job meaning| Job[JobBehaviorTest]
PT -->|composition| Wf[WorkflowBehaviorTest]
PT -->|runtime claim| Swap[MultiDriverBehaviorTest]
Companion · content/nolife-tests
composer install
composer test:good # behavioral insurance
composer test:bad # educational false insurance
php bin/excerpt.php fiber
php bin/excerpt.php amp
Homework
ParseJob on purpose.Worth more than another coverage report.
Honesty
run(Ip) yetClosing
Resources
content/nolife-tests — composer test:goodpressure-test-decisions / testing specializationYour AI writes tests. Make sure they are worth claiming.