Testing integrations with external APIs is one of the most fragile parts of any web application. In theory, we write tests, mock HTTP clients, and feel confident. In practice, APIs change, fields disappear, status codes shift, and production breaks anyway.
I learned this the hard way.
In one project, all my tests were green. My mocks returned exactly what I expected. A week later, the external API removed one field from the response. My mocks didn’t know about the change. Production did.
Mocks told me everything was fine. Reality disagreed.
This is exactly the kind of problem contract testing is meant to solve. In this article, I’ll show you how to use contract testing for external APIs in PHP with Pact, using a real-world Laravel example.
Why Mocking External APIs Is Not Enough
Mocking external APIs is useful. I still do it. It makes tests fast, deterministic, and cheap. But mocks have one fatal flaw:
They only test your assumptions about the API, not the API itself.
Typical problems I’ve seen in production:
- The API removes or renames a field.
- A nested structure changes shape.
- The API starts returning
404instead of200in some edge cases. - The API adds a required field to the request body.
Your mocks will never catch this unless you manually update them. That means mocks alone can silently drift away from reality.
What Is Contract Testing (In Plain English)
Contract testing sits between mocking and full integration tests.
Instead of saying:
“This is what I think the API returns,”
you say:
“This is the contract between my app (consumer) and the API (provider).”
With consumer-driven contract testing:
- The consumer defines what it expects from the API.
- The provider must verify that it actually fulfills this contract.
If the provider changes something that breaks the contract, tests fail before production breaks.
This turns breaking API changes from a runtime surprise into a build-time failure.
Diagram: How contract testing works between a Laravel consumer, Pact mock server, and the real API provider.

Real-World Scenario: Laravel as API Consumer
Let’s assume a Laravel application integrates with an external billing API:
GET /api/customers/{id}
Response:
{
"id": 123,
"email": "john@example.com",
"is_active": true
}
JSONYour Laravel service:
class BillingClient
{
public function getCustomer(int $id): array
{
$response = Http::get("https://billing.example.com/api/customers/{$id}");
return $response->json();
}
}
PHPYour application relies on:
emailis_active
If the provider removes is_active, your app breaks. Mocks won’t catch it. Contract testing will.
Step-by-Step: Contract Testing with Pact in Laravel
Installing Pact for PHP
Install Pact dependencies for your test environment:
composer require --dev pact-foundation/pact-php
You’ll also need the Pact CLI running locally or in CI (usually via Docker).
Writing the Consumer Contract Test
The goal: define what your Laravel app expects from the API.
Example consumer test:
public function test_it_fetches_customer_from_billing_api()
{
$builder = new InteractionBuilder();
$builder
->given('Customer 123 exists')
->uponReceiving('A request for customer 123')
->with([
'method' => 'GET',
'path' => '/api/customers/123',
])
->willRespondWith([
'status' => 200,
'headers' => ['Content-Type' => 'application/json'],
'body' => [
'id' => 123,
'email' => 'john@example.com',
'is_active' => true,
],
]);
$builder->verify(function () {
$client = new BillingClient();
$customer = $client->getCustomer(123);
$this->assertSame('john@example.com', $customer['email']);
$this->assertTrue($customer['is_active']);
});
}
PHPThis test:
- defines the expected request
- defines the expected response
- generates a contract file (Pact file)
This contract is the single source of truth between consumer and provider.
Verifying the Contract on the Provider Side
On the provider side, the API team runs Pact verification:
pact-verifier --provider-base-url=http://localhost:8000 \
--pact-url=./pacts/billing-consumer.json
BashIf the API no longer returns is_active, verification fails.
This is the key difference from mocks:
- mocks only test the consumer
- contract testing tests the agreement
Running Contract Tests in CI
This is where contract testing becomes powerful.
Typical CI flow:
- Consumer tests generate Pact contracts.
- Provider pipeline verifies contracts.
- Pipeline fails if contracts are broken.
This creates a safety net:
API changes cannot be deployed if they break existing consumers.
Common Mistakes with Contract Testing
Over-Specifying Responses
Don’t lock down every field if you don’t need it.
Only define what your app actually uses.
Only Testing Happy Paths
Include error contracts:
- 404 responses
- validation errors
- unauthorized responses
Not Versioning Contracts
Treat contracts like code. Version them.
Not Enforcing Contracts in CI
If no pipeline enforces them, contract testing is just documentation.
When Contract Testing Is Worth It (and When It’s Overkill)
Contract testing is worth it when:
- multiple teams own different services
- APIs evolve frequently
- breaking changes are expensive
It’s probably overkill when:
- the integration is trivial
- you don’t control the provider
- the API is stable and rarely changes
Contract testing is a scalpel, not a hammer. Use it where API stability matters.
How Contract Testing Fits into Your Laravel Testing Strategy
This is how I think about testing layers in real projects:
- Unit tests – business logic
- Feature tests – HTTP flows
- Mocks – isolate slow or unstable services
- Contract tests – protect integrations from breaking changes
Mocks keep your tests fast.
Contract tests keep your integrations honest.
They solve different problems and work best together.
Final Thoughts
Mocking external APIs is necessary — but it’s not sufficient.
If your application depends on external services and you’ve ever been surprised by a breaking API change in production, contract testing is the missing safety net.
You don’t need to contract-test everything. Start with one critical integration. Add one contract. Let your CI enforce it. The first time a breaking change is caught before deployment, contract testing pays for itself.

