waffle-commons / testing
Kernel testing bridge for the Waffle framework: in-process request dispatch through the real pipeline, plus time, queue, mailer and HTTP-client test doubles.
Requires
- php: ^8.5
- phpunit/phpunit: ^12.5
- psr/clock: ^1.0
- psr/http-client: ^1.0
- psr/http-factory: ^1.1
- psr/http-message: ^2.0
- waffle-commons/contracts: 0.1.0-beta7
Requires (Dev)
- carthage-software/mago: ^1.59
- cyclonedx/cyclonedx-php-composer: ^6.2
- igor-php/igor-php: ^0.10.1
- nyholm/psr7: ^1.8
- php-mock/php-mock-phpunit: ^2.16
- vimeo/psalm: ^6.20
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-06 08:34:33 UTC
README
Waffle Testing Component
Release:
0.1.0-beta7ย |ยCHANGELOG.mdStatus: First released in0.1.0-beta7โ roadmap[TEST-01]. A dev-only package (composer require --dev); its test doubles implement the samewaffle-commons/contractsinterfaces as production code.
Kernel testing bridge for the Waffle framework: in-process request dispatch through the real pipeline, plus time, queue, mailer and HTTP-client test doubles.
๐ฆ Installation
composer require --dev waffle-commons/testing
๐งช Testing through the real pipeline
Extend Waffle\Commons\Testing\PHPUnit\WaffleTestCase and build your kernel in createKernel().
Each createClient() boots a fresh kernel; the client sends requests through it in-process, with no
web server and no network:
use App\Factory\AppKernelFactory; use Psr\Clock\ClockInterface; use Waffle\Commons\Contracts\Constant\Constant; use Waffle\Commons\Contracts\Container\ContainerInterface; use Waffle\Commons\Contracts\Core\KernelInterface; use Waffle\Commons\Testing\Clock\FrozenClock; use Waffle\Commons\Testing\PHPUnit\WaffleTestCase; final class OrderApiTest extends WaffleTestCase { protected function createKernel(): KernelInterface { return AppKernelFactory::create(Constant::ENV_TEST); } public function testShowsAnOrder(): void { $clock = new FrozenClock('2026-10-09T09:00:00+00:00'); $client = $this->createClient(static function (ContainerInterface $container) use ($clock): void { $container->set(ClockInterface::class, $clock); }); $response = $client->request('GET', '/orders/42'); self::assertStatus(200, $response); self::assertJsonBody(['id' => 42, 'status' => 'shipped'], $response); } }
- The worker's sequence. Every request runs
handle(), thenterminate()when the kernel is terminable, thenreset(), even when the kernel throws. Sending the same request twice shows whether any state leaks from one request into the next. - Doubles before the lock. The closure given to
createClient()runs between the kernel'sboot()andconfigure(), before the container locks. A double reaches what the container resolves (controllers and their dependencies), not a collaborator your kernel factory built up front. - Requests as production builds them. The client builds requests with the PSR-17
ServerRequestFactoryInterfaceandStreamFactoryInterfacefrom the kernel's container (your kernel factory registers them), and fills what the productionGlobalsFactoryderives: CGI server parameters, query parameters, cookies and the parsed body. Like production, a body is parsed only forPOST: a urlencoded form or a JSON object.jsonRequest()sends$dataas JSON;createRequest()+send()give full control (passREMOTE_ADDRand other server parameters in$server). Multipart bodies are not built. - Assertions:
assertStatus()(quotes the body when the status differs),assertHeader(),assertJsonBody().TestResponsealso exposesstatus(),header(),json()and the rawbody.
๐ญ Test doubles
| Double | Stands in for | Behaviour |
|---|---|---|
Clock\FrozenClock |
Psr\Clock\ClockInterface |
Shows one instant until the test calls set() or advance('+90 seconds'). Production code uses Waffle\Commons\Utils\Clock\SystemClock. |
Http\ReplayClient |
PSR-18 ClientInterface and ConcurrentClientInterface |
Replays the answers the test queued per method and URI (addResponse(), addNetworkError()), each once and in order, and records every request (requests(), pending()). A request with no answer left throws UnexpectedRequestException, which is not a PSR-18 exception, so an unplanned call fails the test. |
Queue\InMemoryQueue |
QueueDispatcherInterface and QueueConsumerInterface |
Records every dispatched envelope (dispatched(), by queue too) with deterministic ids, and serves a worker: receive() follows the injected clock and never blocks, acknowledge() / release() settle a delivery, reclaim() plays the recovery after a worker crash. |
Queue\InMemoryFailedMessageStore |
FailedMessageStoreInterface |
Keeps what a worker gave up on โ full envelope, reason, instant from the clock โ oldest first. |
Mailer\MailerSpy |
MailerInterface |
Sends nothing; records every Email (sent()). |
RateLimit\InMemoryLimiterStateStorage |
LimiterStateStorageInterface |
Token buckets in memory, with the arithmetic of the Redis store (RateLimit-* values, Retry-After) on the injected clock: advance a FrozenClock to refill. One process only โ production keeps limiter state in Redis, never in worker memory. |
๐งญ Architectural boundary (mago guard)
Waffle\Commons\Testing depends only on waffle-commons/contracts and the PSR interfaces โ never on a
sibling component's concrete classes. Every interface it implements lands in waffle-commons/contracts
first. PHPUnit is a runtime dependency of this dev-only package, and the guard permits it in
Waffle\Commons\Testing\PHPUnit only: the kernel client and the doubles work with any test runner.
๐งต Worker safety
The doubles are mutable on purpose: a test sets the time, queues answers and reads what was recorded.
They live for one test and are never resident in a worker, so each mutable property carries
#[WorkerSafe] with that reason, and they do not implement ResettableInterface (the kernel resets
after every request, which would erase what the test asserts). composer igor reports 0 KO.
๐งช Testing
composer mago # formatter, linter, analyzer, guard โ clean means zero output composer tests # PHPUnit 12.5, line coverage โฅ 95 % composer igor # worker-safety audit
๐ Documentation
Full documentation tree: waffle-commons/documentation
๐ License
MIT โ see LICENSE.md.