friendship ended with social-app. php is my new best friend
1---
2title: Testing
3description: Learn how to test code that uses the Fetch HTTP package
4---
5
6# Testing
7
8This guide explains how to test code that uses the Fetch HTTP package. Properly testing HTTP-dependent code is crucial for creating reliable applications.
9
10## Mock Responses
11
12The Fetch HTTP package provides built-in utilities for creating mock responses:
13
14```php
15use Fetch\Http\ClientHandler;
16use Fetch\Enum\Status;
17
18// Create a basic mock response
19$mockResponse = ClientHandler::createMockResponse(
20 200, // Status code
21 ['Content-Type' => 'application/json'], // Headers
22 '{"name": "John Doe", "email": "john@example.com"}' // Body
23);
24
25// Using Status enum
26$mockResponse = ClientHandler::createMockResponse(
27 Status::OK, // Status code as enum
28 ['Content-Type' => 'application/json'],
29 '{"name": "John Doe", "email": "john@example.com"}'
30);
31
32// Create a JSON response directly from PHP data
33$mockJsonResponse = ClientHandler::createJsonResponse(
34 ['name' => 'Jane Doe', 'email' => 'jane@example.com'], // Data (will be JSON-encoded)
35 201, // Status code
36 ['X-Custom-Header' => 'Value'] // Additional headers
37);
38
39// Using Status enum
40$mockJsonResponse = ClientHandler::createJsonResponse(
41 ['name' => 'Jane Doe', 'email' => 'jane@example.com'],
42 Status::CREATED
43);
44```
45
46## Mock Client with Guzzle MockHandler
47
48For testing code that uses the Fetch HTTP package, you can set up a mock handler to return predefined responses:
49
50```php
51use Fetch\Http\ClientHandler;
52use GuzzleHttp\Handler\MockHandler;
53use GuzzleHttp\HandlerStack;
54use GuzzleHttp\Psr7\Response;
55use GuzzleHttp\Client;
56
57// Create a mock handler with an array of responses
58$mock = new MockHandler([
59 new Response(200, ['Content-Type' => 'application/json'], '{"id": 1, "name": "Test User"}'),
60 new Response(404, [], '{"error": "Not found"}'),
61 new Response(500, [], '{"error": "Server error"}')
62]);
63
64// Create a handler stack with the mock handler
65$stack = HandlerStack::create($mock);
66
67// Create a Guzzle client with the stack
68$guzzleClient = new Client(['handler' => $stack]);
69
70// Create a ClientHandler with the mock client
71$client = ClientHandler::createWithClient($guzzleClient);
72
73// First request will return 200 response
74$response1 = $client->get('https://api.example.com/users/1');
75echo $response1->status(); // 200
76echo $response1->json()['name']; // "Test User"
77
78// Second request will return 404 response
79$response2 = $client->get('https://api.example.com/users/999');
80echo $response2->status(); // 404
81
82// Third request will return 500 response
83$response3 = $client->get('https://api.example.com/error');
84echo $response3->status(); // 500
85```
86
87## Testing a Service Class
88
89Here's how to test a service class that uses the Fetch HTTP package:
90
91```php
92use PHPUnit\Framework\TestCase;
93use Fetch\Http\ClientHandler;
94use GuzzleHttp\Handler\MockHandler;
95use GuzzleHttp\HandlerStack;
96use GuzzleHttp\Psr7\Response;
97use GuzzleHttp\Client;
98
99class UserService
100{
101 private ClientHandler $client;
102
103 public function __construct(ClientHandler $client)
104 {
105 $this->client = $client;
106 }
107
108 public function getUser(int $id): array
109 {
110 $response = $this->client->get("/users/{$id}");
111
112 if ($response->isNotFound()) {
113 throw new \RuntimeException("User {$id} not found");
114 }
115
116 return $response->json();
117 }
118
119 public function createUser(array $userData): array
120 {
121 $response = $this->client->post('/users', $userData);
122
123 if (!$response->successful()) {
124 throw new \RuntimeException("Failed to create user: " . $response->status());
125 }
126
127 return $response->json();
128 }
129}
130
131class UserServiceTest extends TestCase
132{
133 private function createMockClient(array $responses): ClientHandler
134 {
135 $mock = new MockHandler($responses);
136 $stack = HandlerStack::create($mock);
137 $guzzleClient = new Client(['handler' => $stack]);
138
139 return ClientHandler::createWithClient($guzzleClient);
140 }
141
142 public function testGetUserReturnsUserData(): void
143 {
144 // Arrange
145 $expectedUser = ['id' => 1, 'name' => 'Test User'];
146 $mockResponses = [
147 new Response(200, ['Content-Type' => 'application/json'], json_encode($expectedUser))
148 ];
149 $client = $this->createMockClient($mockResponses);
150 $userService = new UserService($client);
151
152 // Act
153 $user = $userService->getUser(1);
154
155 // Assert
156 $this->assertEquals($expectedUser, $user);
157 }
158
159 public function testGetUserThrowsExceptionForNotFound(): void
160 {
161 // Arrange
162 $mockResponses = [
163 new Response(404, ['Content-Type' => 'application/json'], '{"error": "Not found"}')
164 ];
165 $client = $this->createMockClient($mockResponses);
166 $userService = new UserService($client);
167
168 // Assert & Act
169 $this->expectException(\RuntimeException::class);
170 $this->expectExceptionMessage('User 999 not found');
171
172 $userService->getUser(999);
173 }
174
175 public function testCreateUserReturnsCreatedUser(): void
176 {
177 // Arrange
178 $userData = ['name' => 'New User', 'email' => 'new@example.com'];
179 $expectedUser = array_merge(['id' => 123], $userData);
180 $mockResponses = [
181 new Response(201, ['Content-Type' => 'application/json'], json_encode($expectedUser))
182 ];
183 $client = $this->createMockClient($mockResponses);
184 $userService = new UserService($client);
185
186 // Act
187 $user = $userService->createUser($userData);
188
189 // Assert
190 $this->assertEquals($expectedUser, $user);
191 }
192}
193```
194
195## Testing History
196
197You can also use `GuzzleHttp\Middleware::history()` to capture request/response history for testing:
198
199```php
200use GuzzleHttp\Middleware;
201use GuzzleHttp\HandlerStack;
202use GuzzleHttp\Client;
203use Fetch\Http\ClientHandler;
204use Psr\Http\Message\RequestInterface;
205
206class ClientHistoryTest extends \PHPUnit\Framework\TestCase
207{
208 public function testRequestContainsExpectedHeaders(): void
209 {
210 // Set up a history container
211 $container = [];
212 $history = Middleware::history($container);
213
214 // Create a stack with the history middleware
215 $stack = HandlerStack::create();
216 $stack->push($history);
217
218 // Add a mock response
219 $mock = new \GuzzleHttp\Handler\MockHandler([
220 new \GuzzleHttp\Psr7\Response(200, [], '{}')
221 ]);
222 $stack->setHandler($mock);
223
224 // Create a Guzzle client with the stack
225 $guzzleClient = new Client(['handler' => $stack]);
226
227 // Create a ClientHandler with the client
228 $client = ClientHandler::createWithClient($guzzleClient);
229
230 // Make a request
231 $client->withToken('test-token')
232 ->withHeader('X-Custom-Header', 'CustomValue')
233 ->get('https://api.example.com/resource');
234
235 // Assert request contained expected headers
236 $this->assertCount(1, $container);
237 $transaction = $container[0];
238 $request = $transaction['request'];
239
240 $this->assertEquals('GET', $request->getMethod());
241 $this->assertEquals('https://api.example.com/resource', (string) $request->getUri());
242 $this->assertEquals('Bearer test-token', $request->getHeaderLine('Authorization'));
243 $this->assertEquals('CustomValue', $request->getHeaderLine('X-Custom-Header'));
244 }
245}
246```
247
248## Testing Asynchronous Requests
249
250For testing asynchronous code:
251
252```php
253use function async;
254use function await;
255use function all;
256
257class AsyncTest extends \PHPUnit\Framework\TestCase
258{
259 public function testAsyncRequests(): void
260 {
261 // Create mock responses
262 $mock = new \GuzzleHttp\Handler\MockHandler([
263 new \GuzzleHttp\Psr7\Response(200, [], '{"id":1,"name":"User 1"}'),
264 new \GuzzleHttp\Psr7\Response(200, [], '{"id":2,"name":"User 2"}')
265 ]);
266
267 $stack = HandlerStack::create($mock);
268 $guzzleClient = new Client(['handler' => $stack]);
269 $client = ClientHandler::createWithClient($guzzleClient);
270
271 // Using modern async/await pattern
272 $result = await(async(function() use ($client) {
273 $results = await(all([
274 'user1' => async(fn() => $client->get('https://api.example.com/users/1')),
275 'user2' => async(fn() => $client->get('https://api.example.com/users/2'))
276 ]));
277
278 return $results;
279 }));
280
281 // Assert responses
282 $this->assertEquals(200, $result['user1']->status());
283 $this->assertEquals('User 1', $result['user1']->json()['name']);
284
285 $this->assertEquals(200, $result['user2']->status());
286 $this->assertEquals('User 2', $result['user2']->json()['name']);
287
288 // Or using traditional promise pattern
289 $handler = $client->getHandler();
290 $handler->async();
291
292 $promise1 = $handler->get('https://api.example.com/users/1');
293 $promise2 = $handler->get('https://api.example.com/users/2');
294
295 $promises = $handler->all(['user1' => $promise1, 'user2' => $promise2]);
296 $responses = $handler->awaitPromise($promises);
297
298 // Assert responses
299 $this->assertEquals(200, $responses['user1']->status());
300 $this->assertEquals('User 1', $responses['user1']->json()['name']);
301
302 $this->assertEquals(200, $responses['user2']->status());
303 $this->assertEquals('User 2', $responses['user2']->json()['name']);
304 }
305}
306```
307
308## Testing with Custom Response Factory
309
310You can create a helper for generating test responses:
311
312```php
313use Fetch\Http\ClientHandler;
314use Fetch\Enum\Status;
315
316class ResponseFactory
317{
318 public static function userResponse(int $id, string $name, string $email): \Fetch\Http\Response
319 {
320 return ClientHandler::createJsonResponse([
321 'id' => $id,
322 'name' => $name,
323 'email' => $email,
324 'created_at' => '2023-01-01T00:00:00Z'
325 ]);
326 }
327
328 public static function usersListResponse(array $users): \Fetch\Http\Response
329 {
330 return ClientHandler::createJsonResponse([
331 'data' => $users,
332 'meta' => [
333 'total' => count($users),
334 'page' => 1,
335 'per_page' => count($users)
336 ]
337 ]);
338 }
339
340 public static function errorResponse(int|Status $status, string $message): \Fetch\Http\Response
341 {
342 return ClientHandler::createJsonResponse(
343 ['error' => $message],
344 $status
345 );
346 }
347
348 public static function validationErrorResponse(array $errors): \Fetch\Http\Response
349 {
350 return ClientHandler::createJsonResponse(
351 [
352 'message' => 'Validation failed',
353 'errors' => $errors
354 ],
355 Status::UNPROCESSABLE_ENTITY
356 );
357 }
358}
359
360// Usage in tests
361class UserServiceTest extends \PHPUnit\Framework\TestCase
362{
363 public function testGetUser(): void
364 {
365 $mockResponses = [
366 ResponseFactory::userResponse(1, 'John Doe', 'john@example.com')
367 ];
368
369 // Create client and test...
370 }
371
372 public function testValidationError(): void
373 {
374 $mockResponses = [
375 ResponseFactory::validationErrorResponse([
376 'email' => ['The email must be a valid email address.']
377 ])
378 ];
379
380 // Create client and test...
381 }
382}
383```
384
385## Testing HTTP Error Handling
386
387Test how your code handles various HTTP errors:
388
389```php
390use Fetch\Exceptions\NetworkException;
391
392class ErrorHandlingTest extends \PHPUnit\Framework\TestCase
393{
394 public function testHandles404Gracefully(): void
395 {
396 $mock = new \GuzzleHttp\Handler\MockHandler([
397 new \GuzzleHttp\Psr7\Response(404, [], '{"error": "Not found"}')
398 ]);
399
400 $stack = HandlerStack::create($mock);
401 $guzzleClient = new Client(['handler' => $stack]);
402 $client = ClientHandler::createWithClient($guzzleClient);
403
404 $userService = new UserService($client);
405
406 try {
407 $userService->getUser(999);
408 $this->fail('Expected exception was not thrown');
409 } catch (\RuntimeException $e) {
410 $this->assertEquals('User 999 not found', $e->getMessage());
411 }
412 }
413
414 public function testHandlesNetworkError(): void
415 {
416 $mock = new \GuzzleHttp\Handler\MockHandler([
417 new \GuzzleHttp\Exception\ConnectException(
418 'Connection refused',
419 new \GuzzleHttp\Psr7\Request('GET', 'https://api.example.com/users/1')
420 )
421 ]);
422
423 $stack = HandlerStack::create($mock);
424 $guzzleClient = new Client(['handler' => $stack]);
425 $client = ClientHandler::createWithClient($guzzleClient);
426
427 $userService = new UserService($client);
428
429 $this->expectException(\RuntimeException::class);
430 $userService->getUser(1);
431 }
432}
433```
434
435## Testing with Retry Logic
436
437Testing how your code handles retry logic:
438
439```php
440use GuzzleHttp\Handler\MockHandler;
441use GuzzleHttp\HandlerStack;
442use GuzzleHttp\Psr7\Response;
443use GuzzleHttp\Client;
444use Fetch\Http\ClientHandler;
445use Fetch\Enum\Status;
446
447class RetryTest extends \PHPUnit\Framework\TestCase
448{
449 public function testRetriesOnServerError(): void
450 {
451 // Mock responses: first two are 503, last one is 200
452 $mock = new MockHandler([
453 new Response(503, [], '{"error": "Service Unavailable"}'),
454 new Response(503, [], '{"error": "Service Unavailable"}'),
455 new Response(200, [], '{"id": 1, "name": "Success after retry"}')
456 ]);
457
458 $container = [];
459 $history = \GuzzleHttp\Middleware::history($container);
460
461 $stack = HandlerStack::create($mock);
462 $stack->push($history);
463
464 $guzzleClient = new Client(['handler' => $stack]);
465 $client = ClientHandler::createWithClient($guzzleClient);
466
467 // Configure retry
468 $client->retry(2, 10) // 2 retries, 10ms delay
469 ->retryStatusCodes([Status::SERVICE_UNAVAILABLE->value]);
470
471 // Make the request that should auto-retry
472 $response = $client->get('https://api.example.com/flaky');
473
474 // Should have 3 requests in history (initial + 2 retries)
475 $this->assertCount(3, $container);
476
477 // Final response should be success
478 $this->assertEquals(200, $response->status());
479 $this->assertEquals('Success after retry', $response->json()['name']);
480 }
481}
482```
483
484## Testing with Logging
485
486Testing that appropriate logging occurs:
487
488```php
489use Monolog\Logger;
490use Monolog\Handler\TestHandler;
491use GuzzleHttp\Handler\MockHandler;
492use GuzzleHttp\HandlerStack;
493use GuzzleHttp\Psr7\Response;
494use GuzzleHttp\Client;
495use Fetch\Http\ClientHandler;
496
497class LoggingTest extends \PHPUnit\Framework\TestCase
498{
499 public function testRequestsAreLogged(): void
500 {
501 // Create a test logger
502 $testHandler = new TestHandler();
503 $logger = new Logger('test');
504 $logger->pushHandler($testHandler);
505
506 // Set up mock responses
507 $mock = new MockHandler([
508 new Response(200, [], '{"status": "success"}')
509 ]);
510
511 $stack = HandlerStack::create($mock);
512 $guzzleClient = new Client(['handler' => $stack]);
513 $client = ClientHandler::createWithClient($guzzleClient);
514
515 // Set the logger
516 $client->setLogger($logger);
517
518 // Make a request
519 $client->get('https://api.example.com/test');
520
521 // Verify logs were created
522 $this->assertTrue($testHandler->hasInfoThatContains('Sending HTTP request'));
523 $this->assertTrue($testHandler->hasDebugThatContains('Received HTTP response'));
524 }
525}
526```
527
528## Integration Tests with Real APIs
529
530Sometimes you'll want to run integration tests against real APIs. This should typically be done in a separate test suite that can be opted into:
531
532```php
533/**
534 * @group integration
535 */
536class GithubApiIntegrationTest extends \PHPUnit\Framework\TestCase
537{
538 private \Fetch\Http\ClientHandler $client;
539
540 protected function setUp(): void
541 {
542 // Skip if no API token is configured
543 if (empty(getenv('GITHUB_API_TOKEN'))) {
544 $this->markTestSkipped('No GitHub API token available');
545 }
546
547 $this->client = \Fetch\Http\ClientHandler::createWithBaseUri('https://api.github.com')
548 ->withToken(getenv('GITHUB_API_TOKEN'))
549 ->withHeaders([
550 'Accept' => 'application/vnd.github.v3+json',
551 'User-Agent' => 'ApiTests'
552 ]);
553 }
554
555 public function testCanFetchUserProfile(): void
556 {
557 $response = $this->client->get('/user');
558
559 $this->assertTrue($response->successful());
560 $this->assertEquals(200, $response->status());
561
562 $user = $response->json();
563 $this->assertArrayHasKey('login', $user);
564 $this->assertArrayHasKey('id', $user);
565 }
566}
567```
568
569## Using Test Doubles
570
571You can create test doubles (stubs, mocks) for your service classes:
572
573```php
574interface UserRepositoryInterface
575{
576 public function find(int $id): ?array;
577 public function create(array $data): array;
578}
579
580class ApiUserRepository implements UserRepositoryInterface
581{
582 private \Fetch\Http\ClientHandler $client;
583
584 public function __construct(\Fetch\Http\ClientHandler $client)
585 {
586 $this->client = $client;
587 }
588
589 public function find(int $id): ?array
590 {
591 $response = $this->client->get("/users/{$id}");
592
593 if ($response->isNotFound()) {
594 return null;
595 }
596
597 return $response->json();
598 }
599
600 public function create(array $data): array
601 {
602 $response = $this->client->post('/users', $data);
603
604 if (!$response->successful()) {
605 throw new \RuntimeException("Failed to create user: " . $response->status());
606 }
607
608 return $response->json();
609 }
610}
611
612class UserServiceTest extends \PHPUnit\Framework\TestCase
613{
614 public function testCreateUserCallsRepository(): void
615 {
616 // Create a mock repository
617 $repository = $this->createMock(UserRepositoryInterface::class);
618
619 // Set up expectations
620 $userData = ['name' => 'Test User', 'email' => 'test@example.com'];
621 $createdUser = array_merge(['id' => 123], $userData);
622
623 $repository->expects($this->once())
624 ->method('create')
625 ->with($userData)
626 ->willReturn($createdUser);
627
628 // Use the mock in our service
629 $userService = new UserService($repository);
630 $result = $userService->createUser($userData);
631
632 $this->assertEquals($createdUser, $result);
633 }
634}
635
636class UserService
637{
638 private UserRepositoryInterface $repository;
639
640 public function __construct(UserRepositoryInterface $repository)
641 {
642 $this->repository = $repository;
643 }
644
645 public function createUser(array $userData): array
646 {
647 // Validate data, process business logic, etc.
648
649 return $this->repository->create($userData);
650 }
651}
652```
653
654## Best Practices
655
6561. **Mock External Services**: Always mock external API calls in unit tests.
657
6582. **Test Various Response Types**: Test how your code handles success, client errors, server errors, and network issues.
659
6603. **Use Status Enums**: Use the type-safe Status enums for clear and maintainable tests.
661
6624. **Use Test Data Factories**: Create factories for generating test data consistently.
663
6645. **Separate Integration Tests**: Keep integration tests that hit real APIs separate from unit tests.
665
6666. **Test Asynchronous Code**: If you're using async features, test both the modern async/await and traditional promise patterns.
667
6687. **Verify Request Parameters**: Use history middleware to verify that requests are made with the expected parameters.
669
6708. **Abstract HTTP Logic**: Use the repository pattern to abstract HTTP logic, making it easier to mock for tests.
671
6729. **Test Response Parsing**: Test that your code correctly handles and parses various response formats.
673
67410. **Test Retry Logic**: Test that your retry configuration works correctly for retryable errors.
675
67611. **Test Logging**: Verify that appropriate logging occurs for requests and responses.
677
678## Next Steps
679
680- Explore [Dependency Injection](/guide/custom-clients#dependency-injection-with-clients) for more testable code
681- Learn about [Error Handling](/guide/error-handling) for robust applications
682- See [Asynchronous Requests](/guide/async-requests) for more on async testing patterns