PHP Symfony Backend Developer
@imenezzine
@imenezzine1
I.Ezzine
Text
I.Ezzine
I.Ezzine
I.Ezzine
Billing per call
Is the external service unavailable?
Is the API subscription expired?
Limited to GET endpoints
I.Ezzine
I.Ezzine
I.Ezzine
I.Ezzine
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
$responses = [
new MockResponse ($body1, $info1),
new MockResponse ($body2, $info2)
];
$client = new MockHttpClient($responses);
// responses in the same order as passed to MockHttpClient
$response1 = $client->request('...');
$response1 = $client->request('...');
use Symfony\Component\HttpClient\Response\JsonMockResponse;
$response = new JsonMockResponse([
'foo' => 'bar',
]);
I.Ezzine
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
use Psr\Log\NullLogger;
class AuthenticatedClientTest extends TestCase
{
const BASE_AUTH_URI = 'https://example.com/auth';
const BASE_REST_URI = 'https://example.com/rest';
public function testRequestWithExpiredToken(): void
{
$authenticatedClient = new AuthenticatedClient(
new MockHttpClient([
function ($method, $url, $options): MockResponse {
$this->assertSame('POST', $method);
$this->assertSame(sprintf('%s/v2/token', self::BASE_AUTH_URI), $url);
$this->assertSame('{"grant_type":"client_credentials","client_id":"clientId","client_secret":"clientSecret","account_id":"accountId"}', $options['body']);
return new MockResponse('{"access_token":"expired_access_token"}', [
'http_code' => 200,
]);
},
function ($method, $url, $options): MockResponse {
$this->assertSame('GET', $method);
$this->assertSame(sprintf('%s/whatever', self::BASE_REST_URI), $url);
$this->assertArrayHasKey('headers', $options);
$this->assertArrayHasKey('normalized_headers', $options);
$this->assertArrayHasKey('authorization', $options['normalized_headers']);
$this->assertArrayHasKey(0, $options['normalized_headers']['authorization']);
$this->assertSame('Authorization: Bearer expired_access_token', $options['normalized_headers']['authorization'][0]);
return new MockResponse('', [
'http_code' => 401,
]);
},
function ($method, $url, $options): MockResponse {
$this->assertSame('POST', $method);
$this->assertSame(sprintf('%s/v2/token', self::BASE_AUTH_URI), $url);
$this->assertSame('{"grant_type":"client_credentials","client_id":"clientId","client_secret":"clientSecret","account_id":"accountId"}', $options['body']);
return new MockResponse('{"access_token":"new_access_token"}', [
'http_code' => 200,
]);
},
function ($method, $url, $options): MockResponse {
$this->assertSame('GET', $method);
$this->assertSame(sprintf('%s/whatever', self::BASE_REST_URI), $url);
$this->assertArrayHasKey('headers', $options);
$this->assertArrayHasKey(0, $options['normalized_headers']['authorization']);
$this->assertSame('Authorization: Bearer new_access_token', $options['normalized_headers']['authorization'][0]);
return new MockResponse('', [
'http_code' => 201,
]);
},
]),
'clientId',
'clientSecret',
self::BASE_AUTH_URI,
'accountId',
new NullLogger()
);
$authenticatedClient->request('GET', '/whatever');
}
}
Testing Method | Pros | Cons |
---|---|---|
Real API | - Realism of tests - Result reliability |
- External dependency - Cost |
Sandbox | - Test isolation - Result reliability |
- Feature limitations - External dependency |
Mock | -Total control - Speed |
- Deviation from Reality - Maintenability ² |
Local API Containerization | - Test isolation - Realism of tests |
- Complex configuration - Local resource dependency |
I.Ezzine
No tests?
I.Ezzine
I.Ezzine
"Don’t Mock What You Don’t Own"
the London School of TDD – the one that loooves mocks.
I.Ezzine
I.Ezzine
I.Ezzine
I.Ezzine
I.Ezzine
I.Ezzine
Overwriting PHP functions (fopen(), file_get_contents(),
curl_exec()...)"
Retrieve all
details of the request
(metadata)
Execute the real request
Retrieve all details of the response
Storage
Request
Response
Basic single client
\VCR\VCR::configure()
->setCassettePath(base_path().'/tests/cassettes');
Storage path
\VCR\VCR::class;
Mode
//record
\VCR\VCR::configure()->setMode('new_episodes');
//record and replay
\VCR\VCR::configure()->setMode('once');
On
Eject
Off
\VCR\VCR::turnOn();
\VCR\VCR::turnOff();
\VCR\VCR::eject();
\VCR\VCR::configure()->setStorage('json');
Storage format
I.Ezzine
\VCR\VCR::configure()->enableRequestMatchers(['method', 'url', 'host']);
\VCR\VCR::configure()
->addRequestMatcher(
'custom_matcher',
function (\VCR\Request $first, \VCR\Request $second) {
// custom request matching
return true;
}
)
->enableRequestMatchers(['method', 'url', 'custom_matcher']);
I.Ezzine
\VCR\VCR::configure()->enableLibraryHooks(['curl'])
I.Ezzine
class VCRTest extends TestCase
{
/**
* @vcr unittest_annotation_test
*/
public function testInterceptsWithAnnotations()
{
// Requests are intercepted and stored into
// tests/fixtures/unittest_annotation_test
$result = file_get_contents('http://google.com');
$this->assertEquals(
'This is a annotation test dummy.',
$result,
'Call was not intercepted (using annotations).'
);
// VCR is automatically turned on and off.
}
}
PHPUnit TestListener for PHP-VCR phpunit-testlistener-vcr
I.Ezzine
I.Ezzine
@vcr
Scenario: retrieve a product
Given 1 product with category "category_1" exist in the PIM
When I retrieve the product from the pim with the root category "test"
Then I should get the expected pim product
I.Ezzine
Feature: creating an image via an API
@vcr
Scenario: Using the recorded response from PHP-VCR
Given I'm using PHP-VCR
When I make a request to the create image API
Then the response should contain the image URL
# config/tests/boostrap.php
use Symfony\Component\Dotenv\Dotenv;
require dirname(__DIR__).'/../vendor/autoload.php';
require __DIR__.'/VCRMatcher/CustomBodyMatcher.php';
\VCR\VCR::configure()
->addRequestMatcher(
'custom_body_matcher',
new CustomBodyMatcher()
)
->enableRequestMatchers(['method', 'url', 'custom_body_matcher', 'post_fields'])
->enableLibraryHooks(['curl'])
->setCassettePath(dirname(__DIR__, 2).'/tests/Environment/IO/cassettes')
->setStorage('yaml')
->setMode('once');
// Turn on for Initializing VCR
\VCR\VCR::turnOn();
// Turn off for DI
\VCR\VCR::turnOff();
(new Dotenv())->bootEnv(dirname(__DIR__).'/../.env');
I.Ezzine
# tests/MockServerContexte.php
private bool $vcrOn = false;
/**
* @BeforeScenario
*/
public function setupVCR(BeforeScenarioScope $scenarioEvent): void
{
if ($scenarioEvent->getScenario()->hasTag('vcr')) {
\VCR\VCR::turnOn();
$this->vcrOn = true;
$scenarioCassette = strtolower(
str_replace(' ', '_', $scenarioEvent->getScenario()->getTitle())
);
$suiteName = $scenarioEvent->getSuite()->getName();
\VCR\VCR::insertCassette($suiteName.'/'.$scenarioCassette.'.yaml');
}
}
/**
* @AfterScenario
*/
public function ejectCassette(): void
{
if ($this->vcrOn) {
\VCR\VCR::eject();
}
\VCR\VCR::turnOff();
}
I.Ezzine
-
request:
method: POST
url: 'https://api.example.com/images'
headers:
Host: api.example.com
Accept-Encoding: ''
X-Auth-Client: your_auth_client
X-Auth-Token: your_auth_token
Accept: application/json
User-Agent: GuzzleHttp/7
Content-Type: 'multipart/form-data; boundary=your_boundary'
body: !!binary |
LS00NGIxNzhkZmRkZTFhNzQ1YWNjZjMwMTM4MmUzOWY4MTUxOTBjYzc1DQ
pDb250ZW50LURpc3Bvc2l0aW9uOiBmb3JtLWRhdGE7IG5hbWU9ImltYWdlX
2ZpbGUiOyBmaWxlbmFtZT0iaWNvbi5wbmciDQpDb250ZW50LUxlbmd0aDogMzEwDQp
Db250ZW50LVR5cGU6IGltYWdlL3BuZw0KDQqJUE5HDQoaCgAAAA1JSERSAAAAEAAAABAIBgA
AAB/z/2EAAAAJcEhZcwAACxIAAAsSAdLdfvwAAADoSURBVDiNpZPRDAAAElFTkSuQmCC
response:
status:
http_version: '1.1'
code: '200'
message: OK
headers:
Content-Type: application/json
Transfer-Encoding: chunked
Connection: keep-alive
Vary: Accept-Encoding
X-Request-ID: 838d78668230cc576f6bd482d1229b45
X-Rate-Limit-Requests-Left: '16101494'
X-Rate-Limit-Time-Reset-Ms: '29999'
X-Rate-Limit-Requests-Quota: '16101495'
X-Rate-Limit-Time-Window-Ms: '30000'
Strict-Transport-Security: 'max-age=31536000; includeSubDomains'
body: '{"data":{"id":1,"product_id":2,"is_thumbnail":false,"sort_order":1,"description":"",
"image_file":"y\/550\/icon__19355.png",
"url_zoom":"https:\/\/cdn11.bigcommerce.com\/s-sxuyyapoo3\/products\/493\/images\
/426\/icon__19355.1637747487.1280.1280.png?c=1","url_standard":"https:\/\/cdn11.bigcommerce.com\/s-
I.Ezzine
PHP-VCR is a powerful tool for simplifying integration testing by recording and replaying HTTP requests.
Whether you opt to use its static methods or integrate it with PHPUnit, PHP-VCR can significantly enhance the stability and consistency of your tests by reducing reliance on external services.
By leveraging it wisely, you can make your tests more reliable and faster.
I.Ezzine
I.Ezzine