This repository is a small example project demonstrating how to mock Symfony’s HttpClient using MockHttpClient and MockResponse.
The goal is to showcase a simple and structured approach to:
- Testing code that depends on an external API
- Simulating different HTTP responses
- Organizing mocks cleanly using response factories
When working with Symfony’s HttpClient, you often need to:
- Test without calling a real external API
- Simulate HTTP errors
- Precisely control the returned responses
This project demonstrates a clean way to achieve that.
git clone https://github.com/loic425/http-client-mocking-example.git
cd http-client-mocking-example
composer installwith real API
symfony console app:book-client
symfony console app:book-client /books/9781804617007with Mocks
symfony console --env=test app:book-client
symfony console --env=test app:book-client /books/9781484206485Get new books
<?phpnamespaceApp\Mock\ResponseFactory;
useApp\Mock\Symfony\HttpClient\ResponseFactory\MockResponseFactoryInterface;
useSymfony\Component\DependencyInjection\Attribute\AsDecorator;
useSymfony\Component\DependencyInjection\Attribute\Autowire;
useSymfony\Component\HttpClient\Response\MockResponse;
useSymfony\Contracts\HttpClient\ResponseInterface;
#[AsDecorator(MockResponseFactoryInterface::class)]
finalclass GetBookCollectionResponseFactory implements MockResponseFactoryInterface
{
privateconststringURI_PATTERN = '#^.+/new$#';
publicfunction__construct(
privatereadonlyMockResponseFactoryInterface$responseFactory,
#[Autowire('%kernel.project_dir%/src/Mock/Files')]
privatereadonlystring$mocksDir,
) {
}
publicfunction__invoke(string$method, string$url, array$options): ResponseInterface
{
if (!preg_match(self::URI_PATTERN, $url)) {
return ($this->responseFactory)($method, $url, $options);
}
return MockResponse::fromFile($this->mocksDir . '/new.json');
}
}Get specific book
<?phpnamespaceApp\Mock\ResponseFactory;
useApp\Mock\Symfony\HttpClient\ResponseFactory\MockResponseFactoryInterface;
useSymfony\Component\DependencyInjection\Attribute\AsDecorator;
useSymfony\Component\DependencyInjection\Attribute\Autowire;
useSymfony\Component\HttpClient\Response\MockResponse;
useSymfony\Contracts\HttpClient\ResponseInterface;
#[AsDecorator(MockResponseFactoryInterface::class)]
finalclass GetBookItemResponseFactory implements MockResponseFactoryInterface
{
privateconststringURI_PATTERN = '#^.+/books/([^/]+)$#';
publicfunction__construct(
privatereadonlyMockResponseFactoryInterface$responseFactory,
#[Autowire('%kernel.project_dir%/src/Mock/Files')]
privatereadonlystring$mocksDir,
) {
}
publicfunction__invoke(string$method, string$url, array$options): ResponseInterface
{
if (!preg_match(self::URI_PATTERN, $url, $matches)) {
return ($this->responseFactory)($method, $url, $options);
}
$isbn = $matches[1];
$file = $this->mocksDir . '/books/' . $isbn . '.json';
if (!is_file($file)) {
thrownew \RuntimeException(sprintf('File "%s" does not exist', $file));
}
return MockResponse::fromFile($this->mocksDir . '/books/' . $isbn . '.json');
}
}To use the mock client in the API:
# services_test.yamlservices:
# Replace book client with the mock oneapp.symfony.mock_http_client.book:
class: Symfony\Component\HttpClient\MockHttpClientdecorates: book.clientarguments:
- '@App\Mock\Symfony\HttpClient\ResponseFactory\MockResponseFactoryInterface'Of course, you will need to create this decoration for non-production envs only.
The interface is very simple.
<?phpdeclare(strict_types=1);
namespaceApp\Mock\Symfony\HttpClient\ResponseFactory;
useSymfony\Contracts\HttpClient\ResponseInterface;
interface MockResponseFactoryInterface
{
publicfunction__invoke(string$method, string$url, array$options): ResponseInterface;
}it's very close to the Symfony\Contracts\HttpClient\HttpClientInterface request method.
The first argument of the MockHttpClient is the response factory, and it accepts a callable. So we just need to create an object which implements our interface to create this callable.
We alias the interface on the Symfony dependency injection system with our first Mock.
<?phpnamespaceApp\Mock\Symfony\HttpClient\ResponseFactory;
useSymfony\Component\DependencyInjection\Attribute\AsAlias;
useSymfony\Component\HttpClient\Response\MockResponse;
useSymfony\Contracts\HttpClient\ResponseInterface;
#[AsAlias(MockResponseFactoryInterface::class)]
finalclass NotImplementedMockResponseFactory implements MockResponseFactoryInterface
{
privateconstintHTTP_NOT_IMPLEMENTED = 501;
publicfunction__invoke(string$method, string$url, array$options): ResponseInterface
{
returnnewMockResponse(body: sprintf('No Mock was found for path "%s" "%s".', $url, $method), info: [
'http_code' => self::HTTP_NOT_IMPLEMENTED,
]);
}
}And then we'll be able to decorate the interface using the AsDecorator attribute from Symfony.
We are now able to implement our custom logic in each decorator using filesystem, or whatever.