@PierreVincent
Contract testing
in practice
with Pact
October 2nd, 2019 – TestBash Manchester
@PierreVincent pvincent.io
@PierreVincent
Pierre Vincent
Infra. & Reliability Manager
@PierreVincent
pvincent.io
From this... … to this.
Login
Service
User
Service
API
GET /users/pierre
{
"user": "pierre",
"name": "Pierre Vincent",
"role": "publisher"
}
200 OK
How do we
test this?
Running in Prod
Tests Pass in
Build
Implement
changes
Deployed in
Prod
User
Service
Login
Service
!
✓ ✓ ✓ ✓
✓
{
...
"role": "editor"
}
{
...
"roles": ["publisher","editor"]
}
Running in Prod
✓ ✓
Maybe we should have tested before deploying to
production…?
Tested what
though?
Users
Service
Login
Service
IP Check
Service
Token
Gen
Service
Cert/Key
Service
Login
Frontend
We only
wanted to
test this bit!
Contract
Login
Service
User
Service
API
Authentication Team Users Team
Consumer Provider
What’s in a contract?
Request Headers
HTTP MethodAPI Endpoint
Query Parameters Request Body
Response HeadersStatus Code Response Body
e.g. /api/users e.g. POST
e.g. ?fields=name e.g. Authorization
e.g. Content-Typee.g. 200
Our running example for the workshop
Game
Service
Leaderboard
Service
APIAPI
Play a game
Return result &
current win-rate
Record game result
Return updated
win-rate
Contract
POST /play
{
"username": "pierre",
"game": "headsOrTails",
"choice": "heads"
}
Game
Service
API
200 OK
Content-Type: application/json; charset=UTF-8
{
"won": true,
"message": "You won!",
"totalPlays": 120,
"totalWins": 71,
"winRate": 59
}
Activity 1
Thinking about API design
Game
Service
Leaderboard
Service
API
Record game result:
Game played, User
playing, Won/Lost
Return win-rate:
Games played,
Games won,
Win-Loss Ratio
“Consumer” Teams
- Document what API you need to send
game results
“Provider” Teams
- Document what API you will
implement to receive game results
Game
Service
Leaderboard
Service
API
Record game result:
Game played, User
playing, Won/Lost
Return win-rate:
Games played,
Games won,
Win-Loss Ratio
“Consumer” & “Provider” Teams
- Meet to compare your API designs
- Discuss differences and come to agreement on the API interaction
- Have you thought of “unhappy” paths? (API errors)
[docs.pact.io]
PACT Specification
Verification philosophy: Tolerant Reader
Implementation guidelines
Implementations
Login
Service
User
Service
A
P
I
Pact
Mock
Server
Authentication Team Users Team
Pact
Consumer
Unit Test
Define
interaction
Trigger
interaction
Generate
Pact
Pact
Provider
Test
Share
Pact
Replay
interaction
Replay &
Verify
Play &
Record
Consumer
Provider
Provider State
Request
Expected
Response
Login Service
User Service
Given that user 'pierre' exists
Method GET
Path /users/pierre
Headers
Accept: application/json
Status 200
Headers
Content-Type: application/json
Body
{
"user": "pierre",
"name": "Pierre Vincent",
"role": "publisher"
}
Interaction
User
Service
A
P
I
1. Set Provider State
2. Send Request
3. Verify Response
Interaction Verification Test
INSERT INTO users [...]
GET /users/pierre
Accept: application/json
{
"user": "pierre",
...
}
{
"user": "pierre",
...
}
?
Activity 2
Implementing the
API Contract
RecordScore API (or write your own)
Game
Service
Leaderboard
Service
POST /recordScore
{
“username”: “anna”,
“game”: “headOrTails”,
“won”: true
}
{
“gamesPlayed”: 123,
“gamesWon”: 55
}
Request
Response
API
Implement API client in Consumer
- In LeaderboardClient.java:
- API endpoint (recordScoreUrl)
- Request Body (RecordScoreRequestBody)
- Response Body (RecordScoreResponseBody)
./gradlew run
Run the service (in game-service dir)
curl -X POST -H "Content-Type: application/json" --data '{"username":
"pierre", "game":"headsortails", "choice": "tails"}'
http://localhost:8080/play
Try your API
Game
Service
API
Creating the Pact
- In LeaderboardClientPactTest.java:
- Complete the PACT definition (RequestResponsePact)
- Update expected response (expectedResponse)
- Update expected request (client.recordScore)
./gradlew test
Run the pact test (in game-service dir)
cat build/pacts/game-service-leaderboard-service.json
Find the generated PACT
Contract
Leaderboard
Service
API
Implement the Provider API
- In ScoreController.java:
- API endpoint (@PostMapping)
- Request Body (RecordScoreRequestBody)
- Response Body (RecordScoreResponseBody)
./gradlew run
Run the service (in leaderboard-service dir)
curl -X POST -H "Content-Type: application/json" --data
'YOUR_JSON_PAYLOAD' http://localhost:8081/your/api/endpoint
Try your API
Verifying the pact
- Copy the generated PACT file (game/build/pacts/*.json)
to the Provider test resources (leaderboard-service/src/test/resources/pacts)
- Complete the PACT definition (RequestResponsePact)
- Update expected response (expectedResponse)
- Update expected request (client.recordScore())
./gradlew test
Run the verification test (in leaderboard-service dir)
Contract
✓
Sharing Contracts
with PactFlow
pactflow.io
Authentication
Dev Team
Users
Dev Team
CMS
Dev Team
Billing
Dev Team
Pact
Broker
PACT
PACT
PACT
PACT
PACT
PACT
PACT
PACT
PACT
PACT
pactflow.io
✓
✓
✓
Dependency
Graphs
Living documentation
by example
✓ Versioning
✓ Tagging
✓ REST Api
Build/Deployment
Pipeline integration
Pact
Broker
pactflow.io
Bonus content
Pacts in CI/CD Pipelines
Provider pipeline
Implement changes Get Pacts from Broker
Replay & Verify
Interactions
Deploy Service
Build
Deploy to EU
PROD-EU
Get Pacts from Broker
Replay & Verify
Interactions
Stop deployment of
incompatible Provider
Stop introduction of
breaking change
PROD-US
PROD-EU
Consumer pipeline
Implement
changes
Generate Pacts
(Build)
Push Pacts to
Broker
Tag Pacts
Each Provider verifies Pacts Deploy Service Tag Pacts
Build
Deploy to EU
HEAD
Stop deployment of
incompatible Consumer
HEAD
PROD-EU
@PierreVincent
Thank you!
Pierre Vincent
Infra. & Reliability Manager
@PierreVincent
pvincent.io

[Test bash manchester] contract testing in practice