you can check via regexes that the string in field...
# general
u
you can check via regexes that the string in field "id" is exactly "1", but that doesn't work with provider states (or at least I couldn't figure out how to make it work) since the pact has the hardcoded string "1", and not as an example
b
This depends how you handle provider states inside your provider. Different language bindings have slightly different options. Depending where you provide the test content for your states (e.g. db stub, domain layer stub, etc), you'll have different options for how those values can be injected, and how static/dynamic you want those states to be.
The original way of declaring interactions doesn't have dynamic path matching, because you need concrete examples to test against. I think that option might be there now, but I don't really use it.
u
in pact-jvm you can specify that a path depends on a provider state (value) like an ID for instance, and that's fine, it makes the provider more flexible in the presence of auto-gen IDs, however what I couldn't quite pin down is the following: the consumer says "when I get entity with ID 1 I will confirm that the response has the same ID", but also keep this ID variable so that the provider is not forced to have an entity with ID 1 created when providing the state
b
Right. There are a couple of separate things going on. • The consumer and provider don't talk to each other at the same time. • Depending where you cut your provider for stubbing, you may or may not need to "have an entity with ID 1 created when providing the state". You don't need to actually persist anything unless you want to, you can just pretend you have a real entity (in-memory), with whatever values your state describes, and return it (across the network boundary). • Not sure if this is what you're implying, but Pact doesn't do behaviour. Each interaction should be agnostic of all others.
To describe an interaction with an ID in the GET (request), which is also returned in the body (response), you just need a matcher in the body (response expectation) with the same ID (by value, not by type). I think that's a canon example in the docs.
(The example with alligators, maybe?)
If you've got some sample code, a concrete explanation might be easier 🙂 I don't know what language/platform you're using, though.
u
right, seen that, perhaps I didn't write my tests correctly, for instance in my consumer (pact-jvm) I have call like
pathFromProvider("/entity/${id}", "/entity/1")
and then in the expectations part I have something like
body.stringMatcher("id", "1")
(using lambdadsl)
that's fine on the consumer side of things
but then it all crumbles on the provider side of things, because I'd expect to be able to return a map like
"id" -> 100
and have the pact be verified no problem
the value of the ID is immaterial, the check here is "when I get entity with ID N, I expect a field "id":"N" in the response"
b
I'll need to look up the docs, but you don't want to match on the String type (then it could be anything, including random unicode). You want a String value matcher. It might be
body.stringValue
or
body.string
.
u
and the reason I was thinking about this sort of test is because I could easily introduce a bug in my provider whereby I change the ID of the returned entity, and this is important on the side of consumers because this ID is shared state across consumers (it's a shopping-cart ID if it helps)
I tried with
stringValue
, but then there was no matchers in the pact.json, which was a bit confusing
but perhaps that's it
b
yes, without matchers, it will match on the literal value 👍
u
even on the provider side of things?
b
Yep, the way the contract describes matching has to work the same on both sides, otherwise it's not reliable. If it doesn't, that's probably a bug.
But also, the contract file isn't really intended for human consumption, so the DSL written there may not be intuitive. The tests should describe the expectations in a more digestible way.
u
I'll play with it a bit more and report back, thanks for your help
🙏 1
ok, I gave it a go and it didn't so what I was hoping it would do
let me try to sanitise the code a bit before sharing
👍 1
Copy code
Consumer:
---------
@Pact(consumer = "C", provider = "P")
RequestResponsePact getExistingLiveBasket(PactDslWithProvider builder) {
  return builder.given("a LIVE basket exists")
                .uponReceiving("retrieve LIVE basket")
                .headers(REQUEST_HEADERS)
                .method("GET")
                .pathFromProviderState("/${basketId}", "/" + existingBasketId)
                .willRespondWith()
                .status(200)
                .headers(RESPONSE_HEADERS)
                .body(LambdaDsl.newJsonBody(
                          body ->
                              body.stringValue("basketId", existingBasketId.toString())
                              .stringMatcher("status", "LIVE"))
                              .build())
                .toPact();
}

@Test
@PactTestFor(pactMethod = "getExistingLiveBasket", pactVersion = PactSpecVersion.V3)
void createBasket_whenServiceIsRunningFine(MockServer server) throws JsonProcessingException {
ResponseBasket responseBasket =
    RestAssured.given()
        .headers(REQUEST_HEADERS)
        .when()
        .get(String.format("%s/%s", server.getUrl(), existingBasketId))
        .then()
        .statusCode(200)
        .extract()
        .as(ResponseBasket.class);

    assertEquals(existingBasketId, responseBasket.getBasketId());
    assertEquals(ResponseBasket.StatusEnum.LIVE, responseBasket.getStatus());
}
that's the consumer test (using pact-jvm junit5 + resassured)
regardless of language, I'm hoping it's self-explanatory
on the provider side of things I got:
Copy code
@State("a LIVE basket exists")
    Map liveBasketExists(Map _ignored_params) {
        Map<String, Object> state = new HashMap<>();

        InternalBasket internalBasket =
            repository.createBasket(TestSupport.Repository.basketWithDefaults()).orElseThrow();

        // the key in this state-map corresponds to the variable in the expression in the consumer test, i.e. in the
        // example consumer test we have a call like .pathFromProviderState("/${basketId}", ...)
        state.put("basketId", internalBasket.getBasketId());

        return state;
    }
which in principle should do what we want (create a basket and then provide its ID back for the pact to be verified)
however the pact has an expected request which uses the ID generated by the consumer
m
when I get entity with ID 1 I will confirm that the response has the same ID
I’m not sure this is an important thing to capture in a contract test. This smells more like a functional test to me
b
I've definitely done this before, even if it smells that way, there's no technical limitation in the Pact spec. This code is over 5 years old, and while JS, does the same thing:
Copy code
describe('can get a specific experiment', () => {
      // given:
      beforeEach(() =>
        provider.addInteraction({
          state: 'a view and two samplers with experiments',
          uponReceiving: 'request to show experiment 0 in sampler 1 in view 0',
          withRequest: {
            method: 'GET',
            path: '/view/0/sampler/1/experiment/0',
            headers: headers.request
          },
          willRespondWith: {
            status: 200,
            headers: headers.response,
            body: {
              name: like('Some experiment'),
              id: '0',
              metrics: eachLike({ type: 'event', name: 'view' }, { min: 1 }),
              variants: eachLike({ name: 'jim', data: '' }, { min: 2 }),
              state: like('unscheduled')
            }
          }
        })
      );

      // when:
      it('', () =>
        client.experiment
          .get(0, 1, 0)
          // then:
          .then(body => {
            expect(body).to.eql({
              name: 'Some experiment',
              id: '0',
              metrics: [{ type: 'event', name: 'view' }],
              variants: [{ name: 'jim', data: '' }, { name: 'jim', data: '' }],
              state: 'unscheduled'
            });
          })
          .catch(fail));
    });
👍 1
That interaction comes out like this in the pactfile:
Copy code
{
      "description": "request to show experiment 0 in sampler 1 in view 0",
      "providerState": "a view and two samplers with experiments",
      "request": {
        "method": "GET",
        "path": "/view/0/sampler/1/experiment/0",
        "headers": {
          "Accept": "application/json"
        }
      },
      "response": {
        "status": 200,
        "headers": {
          "Content-Type": "application/json"
        },
        "body": {
          "name": {
            "json_class": "Pact::SomethingLike",
            "contents": "Some experiment"
          },
          "id": "0",
          "metrics": {
            "json_class": "Pact::ArrayLike",
            "contents": {
              "type": "event",
              "name": "view"
            },
            "min": 1
          },
          "variants": {
            "json_class": "Pact::ArrayLike",
            "contents": {
              "name": "jim",
              "data": ""
            },
            "min": 2
          },
          "state": {
            "json_class": "Pact::SomethingLike",
            "contents": "unscheduled"
          }
        }
      }
    },
So there might be something broken in the Java DSL, or the wrong DSL part is being used. I'm not too sure what the right DSL method is in Java, though.
Ooh, I found one:
Copy code
"gets an existing biller" {
      val pact = builder
        .given("a cached biller list")
        .uponReceiving("a request for biller details").run {
          headers(authHeaders)
          method(GET)
          path("/bpaybiller/24208")
        }
        .willRespondWith().run {
          headers(contentTypeHeaders)
          status(200)
          body(LambdaDsl.newJsonBody { with(it) {
            stringValue("code", "24208")
            stringType("name", "SOUTH EAST WATER")
            stringType("longName", "SOUTH EAST WATER CORPORATION")
          } }.build())
        }
        .toPact()

      verify(pact) {
        fetchBiller(
          GetBillerDetailsRequest(code = "24208")
        ) shouldBe Response.Success(
          BpayBiller(
            code = "24208",
            shortName = "SOUTH EAST WATER",
            longName = "SOUTH EAST WATER CORPORATION"
          )
        )
      }
    }
This one is also over 5 years old, and while Kotlin, is using the same Java8 Lambda DSL, I think.
So,
body.stringValue("code", "24208")
worked for me all the way back then. If it's not working now, there might be a bug.
m
I’d be very surprised if you couldn’t do that in Java
💯 1
u
when I use
body.stringValue(...)
nothing comes out in the pact file, furthermore we're using
pathFromProviderState
. You maybe have a point about a check like "has the same ID as the path I queried" being a non-quite-a-contract-test thing. We can certainly add other sorts of tests that cover this scenario, however I got curious and I suspect many/most our consumers will come up with that question.
b
If
stringValue
doesn't make changes in the Pactfile, it's a bug. It'd be great if you could raise a bug on Github :)
👍 1
u
cool, I'll try to triple-confirm I'm not talking out of my bum and then raise an issue if necessary, but thanks for double-checking my expectations
😆 1
👍 1
ok, did a bit more digging. I didn't look into
stringValue
, but I did look into
valueFromProviderState
which is the way of telling the provider, via the contract: this field will have a value that you need to fill in.
I sort of take it back, this is what we have in the pact now:
Copy code
"matchingRules": {
          "body": {
            "$.basketId": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
which I'm unsure is the right thing
this comes out from a lambda-dsl call like
Copy code
body.valueFromProviderState(
                                                                "basketId",
                                                                "basketId",
                                                                existingBasketId.toString())
(if I leave out the
toString
bit then it's
null
all the way through)
back to the drawing board
m
@uglyog might be able to provide some pointers on the DSL. I think it should also write to a
generators
section
u
If you use
pathFromProviderState
for the path, and then
valueFromProviderState
in the body, with both using the same expression, then there will be two generator entries added to the Pact file that will replace the values with the result from the provider state callback.
u
yes, that's exactly what's happening. One thing though is that if I use
${varName}
in the path, and then
varName
in the value, it doesn't do what I was expecting it to do (in my provider state I return a map containing
varName -> actualValue
). My expectation, after reading the docs for
valueFromProviderState
was that if you just specify a name like
varName
in the expression parameter, it'd use that as key, but that didn't quite work for me. Having them both use
${varName}
works just fine though, which is nice.
another gotcha was that if I do
valueFromProvider('name', '$varName', uuidObject)
then the generator is of type
RAW
(which is fine in principle) however the matchers try to match with
null
, if instead I provide
uuidObject.toString()
then the generator is of type
String
(good stuff) and then the values need to match
I'm probably just not reading the docs properly
u
another gotcha was that if I do valueFromProvider('name', '$varName', uuidObject) then the generator is of type RAW (which is fine in principle) however the matchers try to match with null, if instead I provide uuidObject.toString() then the generator is of type String (good stuff) and then the values need to match
That sounds like a bug,
uuidObject
some type of object?
b
Yeah, if it's a UUID class object, it wouldn't be a JSON type (e.g. String) automatically. I don't know if the lib does coercion like that.
u
it's a
UUID
class, yes. I don't mind raw (doing
Object
sort of equals) but I do mind it being checked against
null
for now
toString
is a workaround though