| name | api-contract-testing |
| description | Expert guidance on consumer-driven contract tests for mobile apps using Pact. Use when mobile clients depend on backend APIs that evolve independently and staging smoke tests are slow or flaky. |
API Contract Testing for Mobile Clients
Instructions
Mobile apps ship asynchronously from their backend. A response-shape change the server team considers "non-breaking" can crash 4 % of installed apps a week later. Consumer-driven contract testing replaces fragile "run the app against staging" checks with fast, deterministic contracts that both sides verify in CI.
1. The Model
- The consumer (mobile app) declares what it needs from each endpoint: request shape, expected response shape, status codes.
- Those expectations are serialized as a pact file (JSON) and published to a broker.
- The provider (backend) replays each interaction against its real service in its own CI and fails if it cannot satisfy the contract.
Pact is the de-facto tool. This skill focuses on the consumer side (mobile).
2. Kotlin (Android / KMP) — Pact-JVM Consumer
@ExtendWith(PactConsumerTestExt::class)
@PactTestFor(providerName = "orders-api", pactVersion = PactSpecVersion.V4)
class OrdersApiPactTest {
@Pact(consumer = "android-app")
fun getOrderPact(builder: PactDslWithProvider): RequestResponsePact =
builder
.given("order 42 exists")
.uponReceiving("get order 42")
.path("/orders/42")
.method("GET")
.willRespondWith()
.status(200)
.headers(mapOf("Content-Type" to "application/json"))
.body(newJsonBody { o ->
o.numberType("id", 42)
o.stringType("status", "PAID")
o.numberType("totalCents", 1999)
}.build())
.toPact()
@Test
fun `client parses order`(mockServer: MockServer) = runTest {
val client = OrdersClient(baseUrl = mockServer.getUrl())
val order = client.getOrder(42)
assertEquals(Order(id = 42, status = PAID, total = Money.cents(1999)), order)
}
}
Publish the resulting pact file to a Pact Broker (pact_broker, Pactflow) from CI.
3. Swift (iOS) — PactSwift
final class OrdersApiPactTests: XCTestCase {
var mockService: MockService!
override func setUp() {
mockService = MockService(consumer: "ios-app", provider: "orders-api")
}
func test_getOrder_parsesBody() throws {
mockService
.uponReceiving("a request for order 42")
.given("order 42 exists")
.withRequest(method: .GET, path: "/orders/42")
.willRespondWith(status: 200, body: [
"id": Matcher.SomethingLike(42),
"status": Matcher.SomethingLike("PAID"),
"totalCents": Matcher.SomethingLike(1999),
])
mockService.run { baseURL, done in
Task {
let client = OrdersClient(baseURL: baseURL)
let order = try await client.getOrder(id: 42)
XCTAssertEqual(order, .init(id: 42, status: .paid, totalCents: 1999))
done()
}
}
}
}
4. Dart (Flutter) — pact_dart
void main() {
final pact = PactMockService('flutter-app', 'orders-api');
setUpAll(pact.setup);
tearDownAll(pact.tearDown);
test('client parses order', () async {
pact
.newInteraction()
.given('order 42 exists')
.uponReceiving('get order 42')
.withRequest(method: 'GET', path: '/orders/42')
.willRespondWith(status: 200, body: {
'id': 42, 'status': 'PAID', 'totalCents': 1999,
});
final client = OrdersClient(baseUrl: pact.mockServerUrl);
expect(await client.getOrder(42),
Order(id: 42, status: OrderStatus.paid, totalCents: 1999));
});
}
5. TypeScript (React Native) — @pact-foundation/pact
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
const { like } = MatchersV3;
const provider = new PactV3({ consumer: 'rn-app', provider: 'orders-api' });
test('client parses order', async () => {
provider
.given('order 42 exists')
.uponReceiving('get order 42')
.withRequest({ method: 'GET', path: '/orders/42' })
.willRespondWith({
status: 200,
body: { id: like(42), status: like('PAID'), totalCents: like(1999) },
});
await provider.executeTest(async (mock) => {
const client = new OrdersClient(mock.url);
expect(await client.getOrder(42)).toEqual({ id: 42, status: 'PAID', totalCents: 1999 });
});
});
6. Matchers
Match on types and presence, not exact values, or the contract becomes a wire-format diff. Use like(...), eachLike(...), iso8601Date(), uuid(). Reserve exact matches for enums and stable identifiers.
7. Publishing & Versioning
- Publish pact files tagged with the consumer's git SHA and branch.
- The provider's CI uses
can-i-deploy to refuse deploying if it breaks a pact a deployed consumer still needs.
- Match pact branches to app release branches so hotfix branches verify against matching provider versions.
8. What Contracts Do NOT Cover
- End-to-end flows across multiple services (use a thin E2E suite).
- Performance, auth, or rate limits (separate tooling).
- UI correctness (snapshot / UI tests).
9. Checklist