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