Skip to content

Instantly share code, notes, and snippets.

@bethesque
Last active January 1, 2016 04:29
Show Gist options
  • Save bethesque/5a35a3c1cb9fdab6dce7 to your computer and use it in GitHub Desktop.
Save bethesque/5a35a3c1cb9fdab6dce7 to your computer and use it in GitHub Desktop.
An idea of what a flexible matching fluent API might look like, and how it could be serialised.
# Notes:
# Headers and status would still be value based matching
# Default to type based matching???
# like(xxx) means xxx and all children would be matched using type based matching
# literal(xxx) means xxx and all children would be matched using exact value matching
# eg(generate, matcher) would be a regular expression match
# each_like([]) would indicate the each element in the array should be like the example given
# A like() could be nested inside a literal() which could be nested inside a like() - at each stage,
# it changes the matching type for all children until it gets switched back.
# Outstanding questions:
# Should array order matter?
# How to specify something like "an array of strings, length unknown" (eg. the backtrace of an exception)
# Ruby code
will_respond_with(
{
status: 200,
headers: {
'Content-Type': 'application/json'
},
body: {
age: 30,
address: literal({
street: '123 Fred St',
suburb: like('Melbourne'),
postcode: '3000',
state: eg('VIC',/[A-Z]{3}/)
}),
phoneNumber: eg("0415 134 234", /\d{4} \d{3} \d{3}/),
favouriteColors: each_like(['red']),
an_empty_array: [],
url: eg('http://localhost:1234/blah', %r{http://.*/blah})
}
})
# Generated JSON
{
"responseMatchingRules": {
"body" : {
"$..*": {"match": "type"},
"$.address": {"match":"value", "allowExtraKeys": false},
"$.address.suburb": {"match":"type"},
"$.phoneNumber": {"regex" : "\d{4} \d{3} \d{3}"},
"$.favouriteColors": {"eachLike": true },
"$.url": {"regex" : "http://.*/blah"}
}
},
"response": {
"body": {
"age": 30,
"address": {
"street": "123 Fred St",
"suburb": "Melbourne",
"postcode": "3000",
"state": "VIC"
},
"phoneNumber": "0415 134 234",
"favouriteColors": [
"red"
],
"an_empty_array": [],
"parents": [
"mum",
"dad"
],
"url": "http: //localhost:1234/blah"
}
}
}
end
@uglyog
Copy link

uglyog commented May 5, 2014

I would like to see a mechanism to use the value from the received request. The idea being that some values need to be present, but we are unable know what the value actually will be because they are auto generated. Thus it would be convienient to say: match("") and then use the value from the request if it matches.

One example is a request to the following resource:

PUT /events/7d83c33d-c006-4f8e-b736-363f4ececde8 HTTP/1.1
Content-MD5: oZGEVpiIWE2KlTsp9aKLag==
X-HMAC-DATE: Mon, 05 May 2014 07:10:34 GMT
Authorization: HMAC BABYLON 105bf4cef0953f9a678b976b0d045d7b4d90b98870cca8db9b73c127b45d5d14
User-Agent: Jakarta Commons-HttpClient/3.1
Host: localhost:9292
Content-Length: 300
Content-Type: application/json; charset=UTF-8

{"event_id":"7d83c33d-c006-4f8e-b736-363f4ececde8","timestamp":"2014-05-05T07:10:34.031Z","host":"rholshausen.engr.acx","ip_address":"0:0:0:0:0:0:0:1%0","event_type":"DOCUMENT:EVENT_VIEW_DOWNLOADED","user":{"id":1879049107,"username":"poleary","organisation":1879048492},"document":{"id":1879104598}}"

The Path includes a UUID of the new resource, so I would like to say:

.upon_receiving("a request to store a new event")
    .with( method: :put, path: matching(/\/events\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/) )

Then if it matches, the generated value in the pact file can be the original request value.

@bethesque
Copy link
Author

I get the impression that this use case would be most useful when using pact to back an integration test on the consumer (I assume that's why you don't know what the event ID is ahead of time), which I don't think is it's best use, because we still get the combinatorial problem. That's why the Condor tests were the worst example of pact usage that we had! I will put it in an issue as a feature that we need to consider for 2.0.0 though.

@rpocklin
Copy link

I've done a bit of work on JSON-based matchers before.

For specifying types (ie. Array / Hash)
what about:

friends: type(Array)
age: type(String)

but it almost feels like you will need a Hash declaration for each property to define a set of rules/matchers:

  • type
  • length
  • regex / pattern(s)
  • custom validations (eg. call a method)

@bethesque
Copy link
Author

There are a couple of reasons I've stuck with the "here's an example" technique rather than the "define the types" technique. One is that pact would have to randomly generate values for the response in the consumer project, and the request when verifying a pact, and randomly generated values are not great for the auto generated documentation. The second is that, as you say, you'd have to start specifying the lengths as well, or you'd get random errors if the generated string was too long. This would add complexity to both the definition and the code. I'm not discounting the idea entirely, but it's not the direction I've been heading in, after giving both concepts a good deal of thought.

@thetrav
Copy link

thetrav commented Feb 1, 2015

Super late to the party on this one.

The good thing about pact 1.0 is that you have a clear document indicating what's being tested structurally.

With the jsonpath matching rules, you've still got your structure, but then you've got a list of rules which are exceptions to the structure. Building a mental model of where those rules apply is challenging, it harms readability, it's less a document and more of a serialised program.

Unfortunately I don't have a good solution, documents are naturally less flexible than programs, and programs are naturally less readable than documents.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment