http-interop/http-factory-tests
Unit tests for HTTP Factory implementations. Add test suite config to phpunit.xml and define factory class constants, then validate your PSR-17 request, response, server request, stream, uploaded file, and URI factories.
First: decide whether you actually need this. A stock Laravel app does not use PSR-17 — Laravel's HTTP layer is built on symfony/http-foundation, and Http::fake() / $this->get() helpers never touch a PSR-17 factory. This package only earns its place when your project explicitly works with PSR-7/PSR-17 messages: PSR-18 clients, third-party SDKs, Saloon/Guzzle bridges you configure yourself, PSR-15 middleware, or custom factory classes you wrote for outbound service-to-service calls. If none of that is true, stop here.
If it is true, install it as a dev dependency:
composer require --dev http-interop/http-factory-tests:^2.4
Check your PHPUnit major first (composer show phpunit/phpunit). The 2.x line requires PHP 8 and PHPUnit 10+; 2.4.0 widens the constraint to allow PHPUnit 13.x. Older 0.7.x/1.x releases exist for PHPUnit 9-era projects — don't try to force 2.4.0 onto an old stack.
Where to look first: the test/ directory inside the package.
ls vendor/http-interop/http-factory-tests/test
Read those files before wiring anything up. They are plain, short PHPUnit test cases, and they are the only real documentation of what is asserted. Then read the README's phpunit.xml snippet — that is the entire configuration surface.
First use case: point the suite at whatever PSR-17 implementation you already have and get a green run. The integration mechanism is not subclassing — it is two things in phpunit.xml: (1) the package's test directory added as a testsuite, and (2) six fully-qualified class-name constants.
<phpunit backupGlobals="true">
<testsuites>
<testsuite name="PSR-17 conformance">
<directory>./vendor/http-interop/http-factory-tests/test</directory>
</testsuite>
</testsuites>
<php>
<const name="REQUEST_FACTORY" value="Nyholm\Psr7\Factory\Psr17Factory"/>
<const name="RESPONSE_FACTORY" value="Nyholm\Psr7\Factory\Psr17Factory"/>
<const name="SERVER_REQUEST_FACTORY" value="Nyholm\Psr7\Factory\Psr17Factory"/>
<const name="STREAM_FACTORY" value="Nyholm\Psr7\Factory\Psr17Factory"/>
<const name="UPLOADED_FILE_FACTORY" value="Nyholm\Psr7\Factory\Psr17Factory"/>
<const name="URI_FACTORY" value="Nyholm\Psr7\Factory\Psr17Factory"/>
</php>
</phpunit>
Run it and count the tests that execute:
vendor/bin/phpunit --testsuite="PSR-17 conformance" --testdox
vendor/bin/phpunit --testsuite="PSR-17 conformance" --list-tests # PHPUnit 10+
A green run with zero discovered tests is the most common false positive. Confirm the count.
Isolate it into its own PHPUnit config file. Laravel's phpunit.xml is already crowded with env vars, DB_CONNECTION=sqlite, <source> includes, and cacheDirectory. Adding six global constants and a vendor testsuite to it pollutes every app test run and drags PHPUnit through vendor files on each php artisan test. Instead, create phpunit.psr17.xml at the project root with just the snippet above (plus a bootstrap if you need one) and run it as a separate command:
vendor/bin/phpunit -c phpunit.psr17.xml
This also gives you a clean knob for backupGlobals="true" without paying that cost on your main suite.
Make it a dedicated CI job. One job, one config file, one command. It runs independently of your application test job, so a PHPUnit or PSR-7 library bump fails in an attributable place rather than inside a 400-test application run.
Wrap DI-heavy factories in a no-argument adapter. This is the pattern you will reach for most days. The suite resolves each constant to a class and instantiates it — your factory realistically takes constructor dependencies (a config value, a temp-dir path, a URI factory it delegates to). Give it a zero-argument shim:
namespace Tests\Support;
use App\Http\Factories\RequestFactory as AppRequestFactory;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\RequestInterface;
final class RequestFactoryAdapter implements RequestFactoryInterface
{
private AppRequestFactory $inner;
public function __construct()
{
// No container, no config() — plain PHP only. See gotchas.
$this->inner = new AppRequestFactory(getenv('SERVICE_BASE_URI') ?: 'https://example.test');
}
public function createRequest(string $method, $uri): RequestInterface
{
return $this->inner->createRequest($method, $uri);
}
}
Then point REQUEST_FACTORY at Tests\Support\RequestFactoryAdapter. Keep the adapters in tests/Support/ under your autoload-dev PSR-4 mapping.
Use the constant swap as an upgrade gate. The highest-value day-to-day workflow: before migrating nyholm/psr7 → guzzlehttp/psr7 → laminas-diactoros, change the six constants to the candidate implementation, run the suite, then change them back. You get a behavioral diff in one command. Do the same in reverse when you bump the library — run the suite against the old library first so you know the baseline is green, then bump.
Test your own factories, not just off-the-shelf ones. If you maintain a custom PSR-17 factory (e.g. one that stamps a default User-Agent or normalizes a base URI), this suite is the cheapest regression net you'll get — it's pure unit work, no database, no HTTP.
Layer your own assertions on top. PSR-17 defines creation only. The suite does not validate withHeader(), withStatus(), withUploadedFiles() immutability, or your project's header/URI normalization rules. Keep those in your normal tests/Unit suite; treat this package as the conformance floor, not the ceiling.
Run it before and after PHPUnit upgrades. Sequence: green on the current PHPUnit → upgrade this package → green → upgrade PHPUnit → green. Two separate commits, two separate green runs, unambiguous blame.
It is configured by constants, not by extending a base class. Blog posts and older notes describing an HttpFactoryTestCase you subclass do not match how this package works today. If you go hunting for an abstract base class you'll waste an afternoon. The entry point is <const name="REQUEST_FACTORY" .../> in phpunit.xml — full stop.
No Laravel bootstrap runs in these tests. These are framework-free PHPUnit cases loaded straight from vendor/. app(), config(), env(), facades, and container bindings are all unavailable. If your factory needs configuration, read it via getenv()/$_ENV and set it in the <php> block, or hardcode a sane default in the adapter. Trying to boot Laravel here is fighting the design.
Zero-argument constructors are effectively mandatory. The suite instantiates your class names with no arguments. A factory requiring a UriFactoryInterface in its constructor will blow up on instantiation, not on an assertion — the error message points at the vendor test, not your class. Adapters solve this; don't try to make your production factory no-arg just to satisfy the test.
Verify the tests actually ran. Version mismatch between this package and your PHPUnit major can result in tests that are discovered-but-skipped, or not discovered at all, and the build still goes green. --testdox and --list-tests are your sanity check. Add this to the CI job so it's mechanical.
Add a smoke test for the constants. A typo in a class name produces a confusing failure deep inside a vendor test. One cheap guard:
public function testFactoryConstantsResolve(): void
{
foreach (['REQUEST_FACTORY', 'RESPONSE_FACTORY', 'SERVER_REQUEST_FACTORY',
'STREAM_FACTORY', 'UPLOADED_FILE_FACTORY', 'URI_FACTORY'] as $const) {
$this->assertTrue(class_exists(constant($const)), "$const does not resolve");
}
}
Don't patch vendor/. When an assertion is genuinely wrong for your implementation, copy the offending test file into your own tests/ directory, adjust it there, and drop that path from the vendor testsuite — or just accept the failure with a documented --filter exclusion. Vendor edits vanish on the next composer install.
Debug by reading the vendor test. Failure messages are generic ("Failed asserting that two strings are identical"). The fastest path to understanding is opening the specific file in vendor/http-interop/http-factory-tests/test/ and reading the expectation. --filter takes the vendor test class name, not yours.
backupGlobals="true" is required and not free. It's in the README for a reason, but it adds per-test overhead. Keeping this suite in its own config file (see above) means your main app suite doesn't inherit the cost.
Keep the vendor directory out of coverage. If you run --coverage against a config that includes this testsuite, PHPUnit will happily walk vendor files. Scope <source> to app/ and src/, and never point coverage at a vendor/ path.
Beware php artisan test --parallel. ParaTest shards by test class; a vendor-hosted testsuite mixed into that run can produce odd discovery and duplicated-worker behavior. Run the conformance suite serially in its own job.
Pin the version and check the constraint on every PHPUnit bump. ^2.4 in composer.json, and after any PHPUnit major upgrade run composer why-not http-interop/http-factory-tests to surface the conflict immediately rather than via a vague resolution error. Use --with-all-dependencies when the resolver needs to move PHPUnit and this package together.
Treat "allows PHPUnit 13.x" as unverified. It means the Composer constraint was widened; it does not prove the assertions pass on that line. If PHPUnit 13 support matters to you, run it yourself on a branch before relying on it — and be aware PHPUnit 13 implies PHP 8.4+, so a project on 8.1–8.3 gets no benefit from that release at all.
Don't expect mutator coverage or SAPI-sensitive checks. PSR-17 says nothing about with*() methods, and uploaded-file tests can't meaningfully exercise is_uploaded_file() outside a real upload SAPI (the package already removed file-mode validation for this reason). Anything touching real multipart handling needs your own tests.
How can I help you explore Laravel packages today?