Contract Tests
Fake and real, tested the same way
When you fake external APIs, how do you know the fake still matches the real thing?
You write a fake payment gateway for fast tests. Months later, the real API changes. Your tests still pass, but production breaks.
The Contract
// app/Contracts/PaymentGateway.php
interface PaymentGateway
{
public function charge(int $amount, string $token): array;
}
The Fake (fast, for unit tests)
// app/Services/Payment/FakePaymentGateway.php
class FakePaymentGateway implements PaymentGateway
{
public function charge(int $amount, string $token): array
{
return [
'status' => 'succeeded',
'id' => 'ch_' . uniqid(),
'amount' => $amount,
];
}
}
The Real Implementation (slow, hits API)
// app/Services/Payment/StripePaymentGateway.php
class StripePaymentGateway implements PaymentGateway
{
public function charge(int $amount, string $token): array
{
$response = Http::withToken(config('services.stripe.secret'))
->post('https://api.stripe.com/v1/charges', [
'amount' => $amount,
'currency' => 'usd',
'source' => $token,
]);
return $response->json();
}
}
The Contract Test — data provider approach
// tests/Contracts/PaymentGatewayContractTest.php
class PaymentGatewayContractTest extends TestCase
{
#[Test]
#[DataProvider('gatewaysProviders')]
public function charges_with_a_valid_payment_token_are_successful($paymentGateway)
{
$charge = $paymentGateway->charge(2500, $paymentGateway->getValidToken());
$this->assertSame(2500, $charge);
}
#[Test]
#[DataProvider('gatewaysProviders')]
public function charges_with_an_invalid_payment_token_fail($paymentGateway)
{
try {
$paymentGateway->charge(2500, 'invalid-payment-token');
} catch (Exception $e) {
$this->assertTrue(true);
return;
}
$this->fail('Charging with an invalid payment token did not throw a PaymentFailedException.');
}
public function gatewaysProviders()
{
return [
'Fake payment gateway' => [new FakePaymentGateway()],
'Stripe payment gateway' => [new StripePaymentGateway('secret-stripe-key')],
];
}
}
Run only fast tests
# Run only the fake variant
php artisan test --filter='gatewaysProviders.*Fake'
# Run only the stripe variant
php artisan test --filter='gatewaysProviders.*Stripe'
Read more by @jcergolj: Contract Tests