An HTTP client faked at the PSR-18 boundary

Mocking a client library’s own interface couples every test to that library; faking the standard interface does not.

final class FakeHttpClient implements ClientInterface
{
    /** @var array<string, ResponseInterface> */
    private array $responses = [];

    public function sendRequest(RequestInterface $r): ResponseInterface
    {
        $key = $r->getMethod() . ' ' . $r->getUri()->getPath();

        return $this->responses[$key]
            ?? throw new RuntimeException("unstubbed: $key");
    }
}

Throwing on an unstubbed request is the design decision that matters — a fake returning an empty response for anything unexpected hides a call the test did not know about. Because the fake implements the PSR interface rather than a library one, swapping the HTTP client implementation later touched the composer file and nothing else.