Relaxing your stubs
Whenever you are maintaining a large cohort of interconnected services, you more often than not will opt for Test Data
e.g. a user with firstname john
and lastname doe
. And then use this same sample user in all your services tests.
That works actually quite well. That's actually what we did previously, for example
in this stub
{
"request": {
"method": "POST",
"urlPath": "/beers",
"headers": {
"content-type": {
"equalTo": "application/json"
}
},
"bodyPatterns": [
{
"equalToJson": {
"name": "Heineken",
"price": 4
}
}
]
},
"response": {
"status": 201,
"jsonBody": {
"id": 2,
"name": "Heineken",
"price": 4
},
"headers": {
"content-type": "application/json"
}
}
}
With such a stub, we can imagine we would end up with a test in our consumer like that:
#[test]
#[stubr::apps("actix-producer")]
fn create_should_create_one() {
let uri: String = actix_producer.uri();
reqwest::blocking::Client::new()
.post(format!("{uri}/beers"))
.json(&json!({
"name": "Heineken",
"price": 4
}))
.send()
.expect_status_created()
.expect_content_type_json()
.expect_body_json(|beer: serde_json::Value| {
assert!(beer.get("name").unwrap().is_string());
assert!(beer.get("price").unwrap().is_u64());
});
}
Notice the request data in this test. Here, this sample is really dummy but in a real application, name
is likely to
come from the consumer's own Test Data. But it cannot be any value ; remember, the producer API requires name
to be
unique. So both producer and consumer Test Data, are now tightly coupled. We are also having specific expectations
regarding the response, for example we are expecting the beer name to be Heineken
and its price 4
(in this situation
you could also relax your test, but sometimes you can't). And in a big system, your consumer is probably also a
producer, so all its stubs are also going to contain Heineken
and 4
.
When things change
Things always change. Sometimes, their format does, for example our beer price can change from to 4
"4.00"
;
that's fine, contract testing is made to catch those changes. But imagine if, for whatever functional reason, now prices
in your system can no longer contain 0 cent by needs to be 99 cent: all your Test Data have to change. Now imagine your
system is made of hundreds of microservices, all requiring this beer service. That's a shame, and all because some Test
Data in a single service has changed.
This could have been avoided if we had taken some time to make our stubs relax its request expectation and also return randomized response data.
Relaxing fields
Here we will get rid of all the hardcoded data in the response (currently there are not enough helpers to also relax the request data with stubr, but it will be possible one day).
We will first randomize the id with anyU32. On the consumer side, this will
generate a random u32
. On the producer side in the verifier tests, it simply asserts that the
field is a u32
.
Now, for the other fields, those are directly taken from the request, so we'll use response templating helpers to forward them from the request.
We end up with a stub like this:
{
"request": {
"method": "POST",
"urlPath": "/beers",
"headers": {
"content-type": {
"equalTo": "application/json"
}
},
"bodyPatterns": [
{
"equalToJson": {
"name": "Heineken",
"price": 4
}
}
]
},
"response": {
"status": 201,
"jsonBody": {
"id": "{{anyI32}}",
"name": "{{jsonPath request.body '$.name'}}",
"price": "{{jsonPath request.body '$.price'}}"
},
"headers": {
"content-type": "application/json"
},
"transformers": ["response-template"]
}
}