Skip to content

Repository files navigation

Symfony Http Client Mocking Example

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

🎯 Why This Repository Exists

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.

🚀 Installation

git clone https://github.com/loic425/http-client-mocking-example.git
cd http-client-mocking-example
composer install

▶️ Test it

with real API

symfony console app:book-client
symfony console app:book-client /books/9781804617007

with Mocks

symfony console --env=test app:book-client
symfony console --env=test app:book-client /books/9781484206485

🕮 Example

Get 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.

⚙️ How does it work?

Create the MockResponseFactoryInterface

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.

Create the first implementation for the fallback

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.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages