Skip to content

Instantly share code, notes, and snippets.

@bethesque

bethesque/1.md Secret

Last active December 5, 2019 10:37
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bethesque/c858e5c15649ae525ef0cc5264b8477c to your computer and use it in GitHub Desktop.
Save bethesque/c858e5c15649ae525ef0cc5264b8477c to your computer and use it in GitHub Desktop.
Draft of non-jvm message pact

Consumer

  1. User declares contract between consumer and provider. Pact builder object is created.
builder {
      serviceConsumer 'MessageConsumer'
      hasPactWith 'MessageProvider'
    }
  1. Using builder object, user sets up expectation of message using Pact DSL (message class is a Pact library object)
given:
    builder {
      given 'the provider has data for a message'
      expectsToReceive 'a confirmation message for a group order'
      withMetaData(contentType: 'application/json')
      withContent {
        name 'Bob'
        date = '2000-01-01'
        status 'bad'
        age 100
      }
  1. User executes tests showing that the consumer can handle the message correctly
builder.run { Message message ->
      def content = new JsonSlurper().parse(message.contentsAsBytes())
      // do something here that handles message
    }

    then:
    true
  1. Builder merges message into pact file using provider state and description as the key (potentially call to pact ruby CLI to convert the simple ruby pact JSON into the proper message format?)

Q. What triggers the writing of the message?

Provider

  1. Iterate through each message a) set up state b) invoke provider code that is expected to produce the message (argument is the message description) c) perform matching of expected/actual d) tear down state

  2. Pass/fail based on overall state

Potential itermediate solution until we switch to Rust

Reuse pact-provider-verifier. Instead of user providing HTTP endpoints for the verifications and the state set up, they provide scripts.

eg.

pact-provider-verifier consumer-provider.json --provider-script provider.js --setup-script setup.js

Argument to the provider-script would be the message description. Argument to the setup-script would be either piped JSON (Q is this possible across platforms?) or key value pairs.

@mefellows
Copy link

mefellows commented Dec 18, 2017

As per the chat yesterday, here is a some pseudo-JS code that tries to demonstrate the thinking behind another solution: the use of a Wrapper proxy that the verifier will call, who's job it is is to map message descriptions to native functions that return the message to be sent.

The verifier can now perform the same job as before:

  1. Locate all the pact files
  2. Iterate through the messages
  3. For each message, setup any state(s) and then execute the message function by hitting an HTTP endpoint - the endpoint is now the Pact Wrapping library, instead of a running provider HTTP code base
// 1. Provider test example
describe('Message queue tests', () => {
  it('should create a bunch of messages', () => {

    // Mappings of message descriptions to functions
    // that will create the message
    //
    // Question: Will these functions ever need some kind
    // of input from Pact systems?
    //
    // Also note, that if we apply a similar approach for HTTP-based
    // synchronous interactions, we could supply request filters here
    // that could modify the payload.
    // The proxy would need to be a bit smarter as it would now be
    // forwarding a request on, but it would give us that power.
    // (not necessary in this case as we can just wrap any of these functions)
    mappings = {
      'Create some customer metadata': someFunc(),
      'Send some metrics': someOtherFunc()
    }

    const setupState = () => { /* ... */ }

    // Expect that the verification process completes sucessfully
    expect(pact.verifyMessages(mappings, setupState)).to.be.fulfilled;
  })
})

// 2. Pact Wrapper implementation code

// verifyMessages pseudocode
const verifyMessages = (mappings, stateFn) => {
  // Get a handle to the CLI verifier
  const verifier = require('some verifier lib')

  // In the Pact Wrapper library, we first start an HTTP server
  // that will invoke the native function code
  const server = require('some server lib').start(8080)
  server.post('/message/', (req, res) => {
    
    // Find function to call and execute it
    const responseBody = mappings[req.body.description]()
    
    // Ensure correct mime type is set, could be anything!
    res.header('Content-Type', 'application/json')

    // return the body
    res.json(responseBody)
  });
  server.post('/state/', (req, res) => {
    // Invoke state setup function
    stateFn(req.body)

    // return success
    res.status(200)
  });

  // Start the verifier process
  verifier.verify({
    url: 'localhost:8080/message/',
    // pactFilesOrDirs: '', // local pacts
    pactBrokerUrl: 'test.pact.dius.com.au',
    pactBrokerUsername: 'test',
    pactBrokerPassword: 'test'
  })
}

@mefellows
Copy link

mefellows commented Dec 18, 2017

As for the consumer side, i'm not convinced this API is sufficient:

given:
    builder {
      given 'the provider has data for a message'
      expectsToReceive 'a confirmation message for a group order'
      withMetaData(contentType: 'application/json')
      withContent {
        name 'Bob'
        date '2000-01-01'
        status 'bad'
        age 100
      }

In the case of WebSockets, yes the interactions are asynchronous, but the response may go back to the original caller which needs to be correctly handled. In these cases, we need to capture the response else we risk breaking the original caller (hopefully without making /provider language is even more confusing!). It feels a bit clunky to have to setup both consumer and provider tests on both sides of this pipe for this interaction style.

WebSockets aside, any request/response style communication would fit into this sort of DSL e.g.

  • Request/Response style messaging over AMQP, Stomp, OpenWire, NATS etc.
  • Text-based protocols like Redis
  • Future/other protocols we have not yet thought about

What if it looked something like this (in JS):

// Usual setup routine
const provider = new Pact({...})

// Create the asynchronous interaction for the test
provider.addAsyncInteraction({
  given: "the provider has data for a message",
  expectsToReceive: "a confirmation message for a group order",
  withMetaData: {
    contentType: "application/json"
  },
  withRequest: {
    name: "Bob",
    date: "2000-01-01",
    status: "bad",
    age: 100
  },
  // Optional response components
  willRespondWith: {
   status: "OK",
   id: 12345678
  },
  withMetaData: {
    contentType: "application/json"
  },
})

The key differences being the addAsyncInteraction to denote a different call type, and an optional response components (willRespondWith and withMetaData) to compare what comes back.

Am I missing something here?

@mefellows
Copy link

Furthermore, what are the potential components in withMetaData that need to be catered for? I'm thinking MIME types are an obvious one, but looking across protocols I could imagine suggestions for all types of things to be included in a specification here.

@mefellows
Copy link

@mefellows
Copy link

@mefellows
Copy link

mefellows commented May 22, 2018

@bethesque here are some notes on implementation as I've seen it from JS/Go-land:

Consumer test:

  • Wrapper language must unmarshal/reverse the matching structure to create the ‘generated’ message to yield back to the message consumer handler.
    • Initially, I thought that the underlying Ruby process would accept the consumer's expectations (as per the HTTP scenario) and then send a request to hook into the consumer function (via the proxy) to verify.
    • resolved: use pact-message reify to get the contents, and then have the wrapper framework invoke the message consumer function
  • sending ‘dir’ during pact-message results in
    • "NoMethodError: undefined method `content' for #<Pact::Interaction:0x00007fc8f1a082e8>”
    • resolved: there was an old pact there in different format, that caused issues
  • How do we standardise/simplify the interface across implementations?
    • is the description -> handler mapping OK? Is there a nicer way?
    • Will it work for v3
  • Do we have to specify the provider (it’s mandatory, but what if we don’t know who it is?)

Provider tests:

  • What response codes should we send to verifier during issue to ensure the verification output is useful. Potential scenarios to consider (and the codes i'm currently using):
    • Handler not found (404)
    • Handler invocation issue (400)
    • Handler internal issue (500)?
    • State not found (404)
    • State invocation issue (400)
    • State setup internal issue (500)
    • Currently, if the handler is not found the wrapper framework needs to respond with an error instead of delegating to the Ruby response
  • Now that we have this infrastructure, can we deprecate the HTTP /setup API endpoints and use this instead? (probably not if people are still using the verifier as a standalone thing)

Terminology:

I've standardised on the following naming conventions:

Term Description
Consumer Message consumer, as per HTTP consumer definition
Provider Message producer, as per HTTP provider definition
StateHandler A function that accepts a state {name: "some state", ...} in order to configure a provider state, and returns an error (throws / rejected Promise...) if it can't
MessageProvider A function that is invoked by the verifier (via message invoker proxy process) that should produce a message or throw an error
MessageConsumer A function on the consumer side that will receive the message payload and process it, throwing an error if it cannot handle it
Message Intermediary The thing that is responsible for passing messages between MessageProviders and MessageConsumers - e.g. MQ, Kinesis. For the purposes of Pact tests, it includes the transport (e.g. tcp) and wire protocol (e.g. JSON/binary)
Message Protocol Adapter A function that is responsible for marshalling/unmarshalling from the Intermediary, and invoking the MessageConsumer/MessageProvider

It would be great to do something similar for framework implementation authors. For instance, your message invoker is what I've been calling the "Native Adapter", as it's job is to convert from an HTTP interface to a native one. I'm not too fussed either way, we just need a common language.

@alexeits
Copy link

alexeits commented Oct 2, 2018

It feels a bit clunky to have to setup both consumer and provider tests on both sides of this pipe for this interaction style.

💯

We use request-response over WebSockets / MQTT quite extensively. We have our own contract testing framework, which we are looking to replace with Pact. While separate pacts for the sides make sense in some cases in the majority of current use cases it would be very cumbersome for us to use message pacts because it will require two message pacts and four time-separated commits for each request-response pair. An optional willRespondWith would be a huge help in solving this issue.

@mefellows
Copy link

Thanks @alexeits and sorry we missed the comment (don't get notifications for Gists!). I'll raise with maintainers now on slack.pact.io

@basickarl
Copy link

basickarl commented Dec 4, 2019

@mefellows Any update on this issue? We are looking to implement the AsyncAPI specification via Websockets and have an API which supports both Publisher/Subscriber and Request/Response patterns!

@bethesque
Copy link
Author

For which language @basickarl? Pact-JS and PactJVM currently support async messages.

@basickarl
Copy link

basickarl commented Dec 5, 2019

bethesque Pact-JS and Pact-Node I was thinking! I apologize but the documentation and examples aren't very clear on how to make one consumer test and one provider test for 2 messages, one request with a payload and one response with a payload.

I would like to write 1 consumer test. The consumer opens a websocket, sends an object asynchronously. The consumer then waits for the response object on the same websocket connection from the mock pact server which it sends asynchronously after the mock pact server received the initial request object over a websocket asynchronously. In other words: asynchronous request-response pattern. Pact supports synchronous request-response from what I see and pub/sub pattern. But I'm talking specifically about asynchronous request-response pattern.

I would like to write 1 provider test. The provider hosts a websocket server to which the mock pact client joins to, the mock pact client then sends an object asynchronously via that websocket. The mock pact client then waits for a response object from the provider which the provider will send asynchronously.

The two tests above can be written in Pact pretty easily, but there is no information how to do this asynchronously via websockets. In the REST consumer test you would define an interaction with withRequest and willRespondWith, but those are specific to REST. I would like the following but tailored to a websocket transferring objects back and fourth over the open websocket connection.

@bethesque
Copy link
Author

@basickarl want to hop on slack.pact.io and chat about it there?

@basickarl
Copy link

@bethesque Absolutely!

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