Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: publish Getting Laminas HttpHandlerRunner to play nice with PHP…
…Unit
- Loading branch information
1 parent
c8e5103
commit 7ddb066
Showing
1 changed file
with
88 additions
and
0 deletions.
There are no files selected for viewing
88 changes: 88 additions & 0 deletions
88
...thoughts/2023-03-13-getting-laminas-http-handler-runner-working-with-phpunit.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
--- | ||
title: Getting Laminas HttpHandlerRunner to play nice with PHPUnit | ||
tags: ["PHP"] | ||
growthStage: budding | ||
--- | ||
|
||
## Preface | ||
At the beginning of this month I de-mothballed both my [[Tuppence|PHP microfamework: Tuppence]] project and the related [[Tuppence Boilerplate|Tuppence Boilerplate Project]]. In doing so started encountering the `Laminas\HttpHandlerRunner\Emitter\SapiEmitter` class throwing a `EmitterException` when invoked within PHPUnit: | ||
|
||
``` | ||
Laminas\HttpHandlerRunner\Exception\EmitterException : Unable to emit response; headers already sent in /vendor/phpunit/phpunit/src/Util/Printer.php:138 | ||
``` | ||
|
||
This doesn't happen when the route being tested is run from a browser which makes me suspect that this is picking up PHPUnit's command line output. | ||
|
||
## Cause | ||
Looking into the stack trace I could see that the `EmitterException` was being thrown by `SapiEmitterTrait::assertNoPreviousOutput()` and quick look inside that function we can see it uses [headers_sent](https://www.php.net/manual/en/function.headers-sent.php) to check if headers have already been sent, throwing the exception if so. | ||
|
||
```php | ||
var_dump(headers_sent()); | ||
echo "Hello world".PHP_EOL; | ||
var_dump(headers_sent()); | ||
|
||
// Outputs: | ||
// bool(false) | ||
// Hello World | ||
// bool(true) | ||
``` | ||
|
||
PHPUnit always outputs its version before running your tests, and it's this that is causing `headers_sent` to return true. | ||
|
||
## Solution | ||
The [[Minimalism|minimalist]] solution is to replace the `SapiEmitter` being used in your tests for a `TestEmitter` that implements the same `EmitterInterface` but doesn't do all the checks that break in a test environment. A benefit of using your own `TestEmitter` is that you can obtain the response and run assertions in your tests. | ||
|
||
```php | ||
<?php | ||
|
||
namespace App\Tests; | ||
|
||
use Psr\Http\Message\ResponseInterface; | ||
use Laminas\HttpHandlerRunner\Emitter\EmitterInterface; | ||
|
||
class TestEmitter implements EmitterInterface | ||
{ | ||
private ResponseInterface $response; | ||
|
||
public function emit(ResponseInterface $response): bool | ||
{ | ||
$this->response = $response; | ||
return true; | ||
} | ||
public function getResponse(): ResponseInterface | ||
{ | ||
return $this->response; | ||
}} | ||
} | ||
``` | ||
|
||
I use this within the Tuppence Boilerplate inside its `BootsApp` extension of `TestCase` this then lets me provide a couple of helpful assertions akin to those seen in Laravel: | ||
|
||
```php | ||
class BootsApp extends TestCase | ||
{ | ||
// ... | ||
|
||
protected function runRequest(ServerRequest $request): string | ||
{ | ||
$this->app->run($request); | ||
return (string)$this->emitter->getResponse()->getBody(); | ||
} | ||
|
||
protected function assertResponseOk(): void | ||
{ | ||
$this->assertEquals( | ||
200, | ||
$this->emitter->getResponse()->getStatusCode() | ||
); | ||
} | ||
|
||
protected function assertResponseCodeEquals($code = 200): void | ||
{ | ||
$this->assertEquals( | ||
$code, | ||
$this->emitter->getResponse()->getStatusCode() | ||
); | ||
} | ||
} | ||
``` |