Extends wiremock-rs by supporting Wiremock json stubs as input.
Use it when you have to mock an external service over http and want a language agnostic format for representing your mocks. Especially shines when the service you have to mock already proposes and publishes Wiremock e.g. Spring Boot with Spring Cloud Contract.
You can use stubr-build to share stubs between a producer project and a consumer one.
Also available as a cli.
First prepare some stubs
bash
echo "{\"request\": {\"method\": \"GET\"}, \"response\": { \"status\": 200 }}" > tests/stubs/hello.json
Then use this stub in your tests.
```rust use isahc; use stubr::; use asserhttp::;
async fn withmacro() { surf::get(stubr.uri()).await.expectstatus_ok(); }
async fn simpleasync() { // supply a directory containing json stubs. Invalid files are just ignored let stubr = Stubr::start("tests/stubs").await; // or just mount a single file let stubr = Stubr::start("tests/stubs/hello.json").await; // or configure it (more configurations to come) let stubr = Stubr::startwith("tests/stubs", Config { port: Some(8080), ..Default::default() }).await; isahc::getasync(stubr.uri()).await.expectstatus_ok(); }
fn simpleblocking() { // can also be used in a blocking way let stubr = Stubr::startblocking("tests/stubs"); let stubr = Stubr::startblockingwith("tests/stubs", Config { port: Some(8080), ..Default::default() }); isahc::get(stubr.uri()).expectstatusok(); } ```
This is a condensed reminder of Wiremock documentation regarding json stubs format. It is also a view of the currently
implemented features in stubr
: just things which actually work in stubr
are present.
You can also get assistance for writing json stubs with IDE completion provided by stubr.
json
{
"id": "82d86e05-9ee0-44ca-9a8d-1fc6f719437e", // (optional) unique stub identifier. Returned in 'Matched-Stub-Id' header
"priority": 1, // (optional) helps solving interlaced conditions (many stubs match the request). 1 is the highest priority, 255 the lowest
"request": {
"method": "GET", // (optional) http method. Can be "ANY" to match any method. Defaults to "ANY"
"urlPath": "/api/exact-url", // exact uri match
"urlPathPattern": "/api/regex-url/([a-z]{4})", // uri must match regex
"urlPattern": "/api/regex-url/([a-z]{4})\\?and=([a-z]{4})", // uri & query must match regex
"url": "/api/url?age=young", // raw url + query parameters by equality matching
"queryParameters": {
"firstname": { "equalTo": "beltram" }, // by equality matching (can also be an int, or a boolean)
"lastname": { "equalTo": "maldant", "caseInsensitive": true }, // case insensitve equality
"age": { "absent": true } // must be absent
"city": { "contains": "a" } // must contain the letter 'a'
"title": { "matches": "([A-Za-z]+)" } // must match regex
"job": { "doesNotMatch": "([A-Za-z]+)" } // or must not match regex
},
"headers": {
"Content-Type": { "equalTo": "application/json" } // by equality matching
// .. then all matchers described above for query parameters are also applicable here
},
"basicAuth" : { // exact Basic authentication matching
"username": "user",
"password": "pass"
},
"jwtAuth": {
"equalTo": "eyJhbGciOiJSUzI1NiJ9.e30.MBkQ..." // plain JWT token
"alg": {
"equalTo": "RS256", // JWT algorithm by equality matcher
"oneOf": ["RS256", "HS256"] // JWT must contain one of these algorithms
},
"payloadPatterns": [
// all matchers available in 'bodyPatterns' ⬇️
]
},
"bodyPatterns": [
{ "equalToJson": {"name": "bob"} }, // strict json request body equality
{ "equalToJson": {"name": "bob"}, "ignoreExtraElements": true }, // ignore extra json fields supplied in request body. Default to false.
{ "equalToJson": {"name": "bob"}, "ignoreArrayOrder": true }, // ignore array items order. Default to false.
{ "matchesJsonPath": "$.name" }, // must just match json path
{ "matchesJsonPath": "$.consoles[?(@.name == 'xbox')]" }, // must match json path + equality
{ "matchesJsonPath": "$.consoles[?(@.price > 200)]" }, // must match json path + bound
{ "expression": "$.name", "contains": "o" }, // must match json path + contain the letter 'o'
{ "expression": "$.user", "equalToJson": { "name": "bob" } }, // must match json path + be equal
{ "binaryEqualTo": "AQID" /* Base 64 */ } // byte array equality
]
},
"response": {
"status": 200, // (required) response status
"fixedDelayMilliseconds": 2000, // delays response by 2 seconds
"jsonBody": { // json response (automatically adds 'Content-Type:application/json' header)
"name": "john",
"surnames": [ "jdoe", "johnny" ]
},
"body": "Hello World !", // text response (automatically adds 'Content-Type:text/plain' header)
"base64Body": "AQID", // binary Base 64 body
"bodyFileName": "tests/stubs/response.json", // path to a .json or .txt file containing the response
"headers": {
"Content-Type": "application/pdf" // returns this response header
},
// ..now response templating
// it uses handlebars and allows you to define dynamic response based upon the content of the request
// it can be used in "jsonBody", "body", "bodyFileName" or "headers"
"transformers": ["response-template"], // required to activate response templating
"jsonBody": {
"url-path-and-query": "{{request.url}}",
"url-path": "{{request.path}}",
"url-path-segments": "{{request.pathSegments.[1]}}", // returns 'two' given '/one/two/three' path
"query": "{{request.query.kind}}", // returns 'comics' given '/api/books?kind=comics'
"multi-query": "{{request.query.kind.[1]}}", // returns 'novel' given '/api/books?kind=comics&kind=novel'
"method": "{{request.method}}", // http request method e.g. "POST"
"header": "{{request.headers.Content-Type}}", // returns request header with given key
"multi-header": "{{request.headers.cache-control.[0]}}", // returns first value of "cache-control" values
"body": "{{request.body}}", // returns raw request body
"from-request": "{{jsonPath request.body '$.name'}}", // takes field 'name' from json request body
"now": "{{now}}", // current datetime (UTC)
"now-fmt": "{{now format='yyyy/MM/dd'}}", // (1) with custom Java SimpleDateFormat
"now-fmt-epoch": "{{now format='epoch'}}", // epoch in milliseconds
"now-fmt-unix": "{{now format='unix'}}", // epoch in seconds
"now-positive-offset": "{{now offset='3 days'}}", // human time positive offset
"now-negative-offset": "{{now offset='-3 days'}}", // human time negative offset
"now-with-timezone": "{{now timezone='Europe/Rome'}}",
"number-is-odd": "{{isOdd 3}}", // or 'isEven'
"string-capitalized": "{{capitalize mister}}", // or 'decapitalize'
"string-uppercase": "{{upper mister}}", // or 'lower'
"number-stripes": "{{stripes request.body 'if-even' 'if-odd'}}",
"string-trim": "{{trim request.body}}", // removes leading & trailing whitespaces
"size": "{{size request.body}}", // string length or array length
"base64-encode": "{{base64 request.body padding=false}}", // padding is optional and defaults to true
"base64-decode": "{{base64 request.body decode=true}}",
"url-encode": "{{urlEncode request.header.x-raw}}",
"url-decode": "{{urlEncode request.header.x-encoded decode=true}}",
// you can also use 'any*' helpers. They will produce a random value
"regex": "{{anyRegex '[a-z]{4}'}}", // generate a random string matching regex
"string": "{{anyNonEmptyString}}", // or '{{anyNonEmptyString}}'
"alphanum": "{{anyAlphaNumeric}}",
"boolean": "{{anyBoolean}}",
"uuid": "{{anyUuid}}",
"ip": "{{anyIpAddress}}", // e.g. '127.0.0.1'
"host": "{{anyHostname}}", // e.g. 'https://github.com'
"email": "{{anyEmail}}", // e.g. 'john.doe@gmail.com'
"enum": "{{anyOf 'alpha' 'beta' 'gamma'}}", // returns randomly one of those 3 values
"number": "{{anyNumber}}", // integer or float
"integer": "{{anyI32}}", // also all Rust int types (u32, u8, i64 etc..)
"float": "{{anyFloat}}",
"anyDate": "{{anyDate}}", // or 'anyTime', 'anyDatetime', 'anyIso8601'
}
}
}
Stubr can be used to record http traffic in your unit tests and dump them into json stubs. Currently, integration is quite limited but much more (actix, warp, rocket, tide) are around the corner.
The recorder acts as a standalone proxy server, so you need to configure your http client to use it.
You can use the record-isahc
feature to get a configured isahc client with
Stubr::record().isahc_client()
or the record-reqwest
feature to get a configured
reqwest client with Stubr::record().reqwest_client()
. Your stubs will then
be stored under target/stubs/localhost
```rust use stubr::Stubr; use isahc;
// this requires record
and record-reqwest
(or record-isahc
) features which are not default.
async fn sampletest() {
Stubr::record().reqwestclient().get(stubr.uri()).send().await.unwrap();
Stubr::record().isahc_client().get(stubr.uri()).unwrap();
// stubs will be created under target/stubs
}
```