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.