Testing & TDD
Testing Microservices
Testing Microservices
Testing microservices architecture presents unique challenges. Services must communicate reliably, contracts must be maintained, and distributed complexity managed. This lesson explores testing strategies for microservices.
Microservices Testing Pyramid
Testing Levels:
- Unit Tests: Test individual service components
- Integration Tests: Test service interactions
- Contract Tests: Verify API contracts
- End-to-End Tests: Test complete user journeys
Contract Testing with Pact
// Consumer test - Order Service
class UserServiceContractTest extends TestCase
{
public function test_get_user_contract()
{
$request = new ConsumerRequest();
$request->setMethod('GET')
->setPath('/api/users/123');
$response = new ProviderResponse();
$response->setStatus(200)
->setBody([
'id' => 123,
'name' => 'John Doe',
'email' => 'john@example.com'
]);
$this->builder
->given('User 123 exists')
->uponReceiving('Request for user 123')
->with($request)
->willRespondWith($response);
$user = $this->userService->getUser(123);
$this->assertEquals('John Doe', $user['name']);
}
}
Service Virtualization
// Mock external service responses
class PaymentServiceMock
{
public static function mockSuccessfulPayment()
{
Http::fake([
'payment-service.com/api/charge' => Http::response([
'transaction_id' => 'txn_123',
'status' => 'success',
'amount' => 100.00
], 200)
]);
}
public static function mockFailedPayment()
{
Http::fake([
'payment-service.com/api/charge' => Http::response([
'error' => 'Insufficient funds'
], 402)
]);
}
}
Testing Service Communication
// Test HTTP communication between services
public function test_order_service_calls_user_service()
{
Http::fake([
'user-service.com/api/users/*' => Http::response([
'id' => 1,
'name' => 'Test User'
])
]);
$order = Order::factory()->create();
$result = $this->orderService->processOrder($order);
Http::assertSent(function ($request) {
return $request->url() == 'http://user-service.com/api/users/1';
});
}
Testing Message Queues
// Test event publishing
public function test_order_created_event_is_published()
{
Event::fake([OrderCreated::class]);
$order = $this->orderService->createOrder([
'user_id' => 1,
'total' => 100
]);
Event::assertDispatched(OrderCreated::class, function ($event) use ($order) {
return $event->order->id === $order->id;
});
}
Testing Circuit Breakers
// Test circuit breaker pattern
public function test_circuit_breaker_opens_on_failures()
{
Http::fake([
'external-service.com/*' => Http::response([], 500)
]);
// Trigger multiple failures
for ($i = 0; $i < 5; $i++) {
try {
$this->service->callExternalApi();
} catch (Exception $e) {
// Expected failures
}
}
// Circuit should be open
$this->assertTrue($this->service->isCircuitOpen());
// Next call should fail fast
$this->expectException(CircuitBreakerOpenException::class);
$this->service->callExternalApi();
}
Testing Service Discovery
// Test service registry
public function test_service_discovery()
{
$registry = new ServiceRegistry();
$registry->register('user-service', 'http://localhost:8001');
$registry->register('order-service', 'http://localhost:8002');
$userServiceUrl = $registry->discover('user-service');
$this->assertEquals('http://localhost:8001', $userServiceUrl);
}
Microservices Testing Tips:
- Use contract testing to prevent integration issues
- Mock external services in unit tests
- Test failure scenarios and timeouts
- Verify retry mechanisms
- Test service degradation gracefully
Common Pitfalls:
- Not testing network failures
- Ignoring eventual consistency
- Over-reliance on end-to-end tests
- Not versioning API contracts
- Missing timeout configurations
Exercise: Create a contract test between two services. Test successful communication, error handling, and timeout scenarios.