Darkwood · PHP · PHPUnit · agents · Flow

From No-Life
Testing to
Behavioral Testing

AI made writing tests cheap.
Choosing what deserves an oracle is now the hard part.

Jobs
≠
Drivers
@matyo91

Opening

Most unit tests are
protecting the
wrong thing.

Not because PHPUnit is weak.
Because the oracle chose implementation over meaning.

01 | provocation @matyo91

Fabien Potencier · Sebastian Bergmann

Your AI writes tests.
Are they worth anything?

Liability

Weak tests under agent velocity

Oracle

What decides “correct”

02 | the question @matyo91

Scope

Not a tutorial. Not a bake-off.

✗ PHPUnit basics
✗ TDD sermon
✗ ReactPHP vs Amp
✓ Oracles that survive refactoring

Jobs = behavior · Drivers = furniture

03 | scope @matyo91

The economics flipped

Writing got cheap. Thinking got scarce.

Before AI

Cost of writing
≫ cost of choosing oracles

With agents

Cost of writing
≪ cost of choosing oracles

Volume without oracles is false insurance.

04 | economics @matyo91

Scarce skill

Not more tests.
Better oracles.

Which behaviors deserve insurance?
Which assertions survive a refactor?
Which doubles hide a design smell?
Would green still pass if meaning broke?
05 | judgment @matyo91

Vocabulary

No-life testing

A green suite that pins implementation,
call order, and runtime plumbing —

while leaving business meaning unasserted.

Alive in CI. Dead as insurance.

06 | no-life @matyo91

Bad · 01 · ImplementationCoupledTest

Mocks all the way down

$parse->expects($this->once())
    ->method('__invoke')
    ->with($raw)
    ->willReturn($parsed);
// …
$this->assertTrue(true); // Green. Never asserted meaning.

Broken real ParseJob → suite stays green.

07 | bad-mocks @matyo91

Bad · 02 · RuntimeCoupledTest

Testing the furniture

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.

08 | bad-runtime @matyo91

Bad · 03 · CoverageTheaterTest

Shape without meaning

$this->assertInstanceOf(ExcerptResult::class, $result);
$this->assertNotNull($result->title);
$this->assertIsString($result->excerpt);
// Never asked: is the excerpt faithful?

Coverage theatre. Agents love it.

09 | bad-theatre @matyo91

Same workflow · three lies

False insurance, catalogued

Call order

Mocks erase Jobs

await()

Pins the Driver

Shape

Green on nonsense

What should we assert instead?

10 | three-lies @matyo91

Flow · Jobs vs Drivers

Behavior lives in Jobs

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
11 | jobs-vs-drivers @matyo91

Workflow · not furniture

Input · Steps · Output

Request
→
Fetch
→
Parse
→
Excerpt
→
Validate
→
Result
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.

12 | workflow @matyo91

Good · JobBehaviorTest

Observable transforms

$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.

13 | job-oracle @matyo91

Good · WorkflowBehaviorTest

Faithfulness, not wiring

$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.

14 | workflow-oracle @matyo91

Good · MultiDriverBehaviorTest

Same oracle. Two runtimes.

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.

15 | multi-driver @matyo91

Beyond Flow

The runtime will change.
Your architecture should not.

Sequential PHP

Call stack ≠ outcome

ReactPHP

Promises / loop furniture

Amp

Amp types ≠ domain packets

Framework X

HTTP adapter ≠ product

If the suite breaks while product meaning is unchanged — you tested furniture.

16 | runtime-independence @matyo91

Doubles under agents

Stub the port.
Don’t mock the Jobs.

Do

InMemoryDocumentSource
Unit-test App\Job\*
composer test:good

Don’t

Mock DriverInterface
Assert job call order
Treat Bad as insurance

$source = new InMemoryDocumentSource([
    'fixture://article' => $html,
]);
17 | stubs @matyo91

Guillaume Moigneu · Agent Skills

Pressure-test the decision.
Don’t generate the suite.

Use $pressure-test-testing-decisions on
tests/Bad/ImplementationCoupledTest.php —
should we keep, rewrite, or delete it?
Oracle
Furniture
Coverage theatre
Driver swap
18 | pressure-test @matyo91

Useful grill · one at a time

Defend the insurance claim

  • Should this be tested at all?
  • Would this fail for the right reason?
  • Does this assertion survive a refactoring?
  • Are these mocks hiding a design problem?
  • Fiber → Amp: would this fail — and should it?
  • Suite green, product wrong — which test lied?
19 | grill @matyo91

One habit change

Rewrite the brief

Weak

“Add unit tests.
Aim for high coverage.”

Strong

“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]
20 | brief @matyo91

Companion · content/nolife-tests

Clone it. Break it. Decide.

composer install
composer test:good   # behavioral insurance
composer test:bad    # educational false insurance
php bin/excerpt.php fiber
php bin/excerpt.php amp
workflows
tests Good/Bad
skills/
fixtures
21 | repository @matyo91

Homework

Break ParseJob on purpose.

Bad
→
stays green
Good
→
goes red

Worth more than another coverage report.

22 | experiment @matyo91

Honesty

What this does not prove

No sync run(Ip) yet
Stubs at ports still useful
Driver CI ≠ app suite
Not an Amp vs Fiber bench
AI can write good tests
Risk = volume without oracles
23 | limits @matyo91

Closing

The value of a test
is not that it passes.

It is that it fails
for the right reason.

Jobs
→
Oracles
→
Insurance
@matyo91

Resources

Go deeper

  • Article — From No-Life Testing to Behavioral Testing
  • content/nolife-tests — composer test:good
  • Darkwood Flow — darkwood-com.github.io/flow
  • Potencier — Weak tests are now a liability
  • Bergmann — Seeing the Truth: Test Oracles
  • Moigneu — pressure-test-decisions / testing specialization

Your AI writes tests. Make sure they are worth claiming.

24 | resources @matyo91