stubr

Wiremock rewritten in Rust


docs.rs docs coverage


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.

use it

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_std::test]

[stubr::mock] // <- you can also provide stubs path here e.g. #[stubr::mock("hello.json")]

async fn withmacro() { surf::get(stubr.uri()).await.expectstatus_ok(); }

[async_std::test]

[stubr::mock]

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(); }

[test]

[stubr::mock]

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(); } ```

wiremock cheat sheet

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' } } }

recording

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.

[tokio::test(flavor = "multi_thread")] // required for recording

[stubr::mock] // start a standalone http server to record, for example stubr itself

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 } ```