The Challenge of Third-Party Integrations
Modern applications depend on external services: payment processors (Stripe, PayPal), email providers (SendGrid, Mailgun), cloud services (AWS, GCP), authentication (Auth0, Okta), and many more. Testing these integrations is challenging because you do not control the external service.
Key testing concerns:
- You cannot test against production without side effects.
- External services may be slow, unavailable, or rate-limited.
- API responses can change without notice.
- Test environments may behave differently from production.
Sandbox and Test Mode Environments
Most major API providers offer sandbox or test environments:
| Provider | Test Mode | Key Feature |
|---|---|---|
| Stripe | Test API keys (sk_test_…) | Fake payments, no real charges |
| PayPal | Sandbox accounts | Simulated transactions |
| SendGrid | Sandbox mode | Emails not actually sent |
| Twilio | Test credentials | SMS not actually sent |
| AWS | LocalStack | Local AWS service emulation |
Using Stripe Test Mode
# Test mode card numbers
4242424242424242 # Always succeeds
4000000000000002 # Always declines
4000000000009995 # Insufficient funds
# Test API call
curl https://api.stripe.com/v1/charges \
-u sk_test_your_key: \
-d amount=2000 \
-d currency=usd \
-d source=tok_visa
Testing Strategies
Strategy 1: Sandbox Testing
Use the provider’s sandbox for realistic integration testing. This tests your actual integration code against the provider’s test environment.
Pros: Most realistic, catches serialization and protocol issues. Cons: Depends on sandbox availability, may have rate limits, slower than mocks.
Strategy 2: Mock Server
Replace the external API with a local mock that returns predefined responses.
// WireMock or similar tool
const mock = nock('https://api.stripe.com')
.post('/v1/charges')
.reply(200, {
id: 'ch_test_123',
amount: 2000,
status: 'succeeded',
});
// Your code calls Stripe — WireMock intercepts and returns mock response
const result = await paymentService.charge(2000, 'usd', 'tok_visa');
expect(result.status).toBe('succeeded');
Pros: Fast, reliable, no external dependency. Cons: May not catch real API behavior differences.
Strategy 3: Record and Replay
Record actual API interactions, then replay them in tests:
// Record mode: real API call, response saved to file
// Replay mode: file response used instead of real API
Tools: Polly.js, VCR (Ruby), Betamax (Java).
Testing Failure Modes
The most important integration tests verify failure handling.
Timeouts
test('handles Stripe timeout gracefully', async () => {
nock('https://api.stripe.com')
.post('/v1/charges')
.delay(10000) // 10 second delay
.reply(200, { id: 'ch_123' });
// Your service should timeout after 5 seconds
const result = await paymentService.charge(2000, 'usd', 'tok_visa');
expect(result.status).toBe('timeout');
expect(result.retryable).toBe(true);
});
Rate Limiting
test('handles 429 rate limit with backoff', async () => {
nock('https://api.stripe.com')
.post('/v1/charges')
.reply(429, { error: 'rate_limit' }, { 'Retry-After': '5' });
const result = await paymentService.charge(2000, 'usd', 'tok_visa');
expect(result.status).toBe('rate_limited');
expect(result.retryAfterSeconds).toBe(5);
});
Service Unavailable
test('handles 503 with circuit breaker', async () => {
// Simulate 5 consecutive 503s
for (let i = 0; i < 5; i++) {
nock('https://api.stripe.com')
.post('/v1/charges')
.reply(503, { error: 'service_unavailable' });
}
// After 5 failures, circuit breaker should open
const result = await paymentService.charge(2000, 'usd', 'tok_visa');
expect(result.status).toBe('circuit_open');
});
API Version Changes
Test that your integration handles unexpected response formats:
test('handles unexpected response fields gracefully', async () => {
nock('https://api.stripe.com')
.post('/v1/charges')
.reply(200, {
id: 'ch_123',
amount: 2000,
status: 'succeeded',
new_field_not_in_docs: 'surprise', // API added a new field
});
const result = await paymentService.charge(2000, 'usd', 'tok_visa');
expect(result.status).toBe('succeeded'); // Should not crash
});
Exercise: Third-Party Integration Testing Suite
Setup
Build a payment service that integrates with Stripe (test mode) and test it comprehensively.
Task 1: Happy Path Testing
Using Stripe test mode, verify:
- Successful payment with test card 4242424242424242.
- Declined payment with test card 4000000000000002.
- Payment with 3D Secure required (4000002760003184).
- Refund of a successful payment.
Task 2: Failure Mode Testing
Using mocks, test:
- Stripe API timeout (5+ seconds) — your service retries once then returns error.
- 429 rate limit — your service respects Retry-After.
- 500 server error — your service retries with exponential backoff.
- Network error (connection refused) — your service returns a clear error.
- Invalid response body (malformed JSON) — your service handles gracefully.
Task 3: Circuit Breaker Implementation and Testing
- Implement a circuit breaker around Stripe API calls.
- After 3 consecutive failures, the circuit opens.
- While open, requests immediately return a fallback response.
- After 30 seconds, the circuit enters half-open state (allows one request).
- If that request succeeds, the circuit closes (normal operation).
Task 4: Monitoring and Alerting
Design monitoring for your third-party integration:
- Track success rate, error rate, and latency per provider.
- Alert when error rate exceeds 5%.
- Alert when average latency exceeds 2x the baseline.
- Log all failed requests with enough context to debug.
Deliverables
- Payment service code with Stripe integration.
- Test suite covering happy paths and all failure modes.
- Circuit breaker implementation with tests.
- Monitoring dashboard design or specification.