Pact-js: Multiple provider interactions for testing gateway as a consumer

Created on 15 Oct 2019  路  17Comments  路  Source: pact-foundation/pact-js

Software versions

  • OS: Mac OSX 10.14.6
  • Consumer Pact library: Pact JS v9.1.1
  • Provider Pact library: n/a
  • Node Version: 10.16.0

Consumer side tests are an API gateway that is responsible for aggregating and cleaning data it requests from microservices before sending the data along to the front end. In order to do this, many methods require making requests to one or more microservices in the process of making, for instance, a POST request.

In my test, I have a POST request which, in order to gather the required data, makes 3 additional GET requests from 2 different microservices before posting the data to the Provider microservice. To try to facilitate this, I registered the 3 GET requests as Provider interactions so I could mock the response on the Pact mock server, but it appears the afterEach() hook executes after each interaction is matched and fulfilled. So the first GET request required to gather data for the whole POST is matched, the interactions are attempted to be verified, and the test fails before making it even halfway through.

I can see that this is the way Pact is intended to run, but in a scenario like this, how am I expected to be able to test a gateway as a consumer? I have seen a very small amount of documentation about multiple Providers in the mock service repo but documentation is sparse.

What is the currently accepted solution for setting something like this up?

Awaiting feedback

All 17 comments

but it appears the afterEach() hook executes after each interaction is matched and fulfilled

The hook should run after each individual test (e.g. an it block) runs, and is independent of the Pact interaction lifecycle managed outside of whatever test framework you're using. If your scenario needs to make more than 1 call to a provider, ensure that happens within a single test.

Most importantly, the call to the verify() method should only be made once all 3 requests have been issued.

If you had some code to show me, i'd happily steer you in the right direction.

describe("when a call to create a new special is made", () => {
    before(() => {
      provider.addInteraction({
        uponReceiving: "a request to add a special",
        state: "the special doesn't exist",
        withRequest: {
          method: "POST",
          path: "/specials",
          body: like(specialsGetExpectation)
        },
        willRespondWith: {
          status: 200,
          headers: {
            "Content-Type": "application/json"
          },
          body: like(specialsGetExpectation)
        }
      });
      provider.addInteraction({
        uponReceiving: "a request to get products",
        withRequest: {
          method: "GET",
          path: `/products/${clientId}`
        },
        willRespondWith: {
          status: 200,
          headers: {
            "Content-Type": "application/json"
          },
          body: like(productsJson)
        }
      });
      provider.addInteraction({
        uponReceiving: "a request to get strains",
        withRequest: {
          method: "GET",
          path: `/strains/${clientId}?clientId=${clientId}`
        },
        willRespondWith: {
          status: 200,
          headers: {
            "Content-Type": "application/json"
          },
          body: eachLike(strainsListExpectation)
        }
      });
      provider.addInteraction({
        uponReceiving: "a request to get price profiles",
        withRequest: {
          method: "GET",
          path: `/strains/${clientId}/price_profiles?clientId=${clientId}`
        },
        willRespondWith: {
          status: 200,
          headers: {
            "Content-Type": "application/json"
          },
          body: eachLike(priceProfilesListExpectation)
        }
      });
    });
    it("creates a new special", () => {
      const ctx = {
        req: {
          user: {
            "https://company.co/app_metadata": {
              clientId: "internalCLientId"
            }
          }
        },
        request: {
          body: specialsGetExpectation
        }
      };
      postSpecials(ctx, () => {});
      expect(ctx.body).to.be.a("object");
    });
  });

So this is the test case I am working with and the method postSpecials() is the one which also makes requests to the three other microservices. From the log file:

I, [2019-10-14T15:46:23.199466 #2466]  INFO -- : Received request GET /products/tK59TzwSGQj3EGucJ
I, [2019-10-14T15:46:23.199879 #2466]  INFO -- : Found matching response for GET /products/tK59TzwSGQj3EGucJ

The first GET request is matched and the request is made successfully on the mock server. Immediately the next log is verification failing:

W, [2019-10-14T15:46:23.201611 #2466] WARN -- : Verifying - actual interactions do not match expected interactions.

From what I could tell, though, this is going to make the pact file itself look a little bit wonky because right now it contains the interaction for GET products.

Is it possible to break up postSpecials so that you can write three pact tests?

@TimothyJones It would be possible but I wouldn't consider it a good testing practice to modify the architecture to fit with this. The only way I can see an implementation of breaking up that method would require a lot of data to be passed around within the gateway which would certainly make the architecture more fragile. Since the nature of the gateway is to aggregate data, it would add a layer of complexity that I would prefer to steer clear of.

Thanks @travwritescode. I understand the issue, I think. There are a couple of things jumping out at me:

  1. Could you please paste the entire log file? It's unclear exactly what sequence of events is happening against the mock server. I'm concerned that promises may not be properly handled, which is one the verification is happening before all of the calls have been made to the server
  2. If I'm not mistaken, your API gateway (in this case the consumer), has dependencies on 3 separate providers (and 4 different API calls). The code above would suggest you're not setting up three separate providers though (i.e. the provider.addInteraction(...) is the same provider and all interactions would be recorded against the one provider). Something like strainsProvider.addInteraction(), productProvider.addInteraction() and specialsProvider.addInteraction() I think is what you want
  3. Maybe you've simplified things for clarity, but it looks like your promises aren't properly handled (all of the addInteraction(...) calls are promises, and your before block should wait on this

@mefellows So I pared down my example to a single GET request which now no longer works for me either. I apologize, I made changes to the original example but cannot get it back to the state it was in before. The error I am receiving is essentially the same, though as the interaction appears to match but is not being recognized by the Provider. (I have replaced some values to protect internal information)

SpecialsCRUD.test.pact.js

const path = require("path");
const { Pact, Matchers } = require("@pact-foundation/pact");
const {
  getSpecials,
  postSpecials
} = require("../../src/controllers/specialsController"); // This is your client-side API layer
const LOG_LEVEL = process.env.LOG_LEVEL || "WARN";
const expect = require("chai").expect;

describe("Specials CRUD", () => {
  const provider = new Pact({
    consumer: "Company Management Gateway",
    provider: "Specials CRUD",
    port: 3000,
    log: path.resolve(process.cwd(), "logs", "mockserver-integration.log"),
    dir: path.resolve(process.cwd(), "pacts"),
    logLevel: LOG_LEVEL,
    spec: 2
  });

  const { eachLike, like } = Matchers;
  const clientId = "someClientId";

  const specialsGetExpectation = {
    _id: "someId",
    clientId: "someClientId",
    createdAt: "2018-04-05T15:01:39.724Z",
    updatedAt: "2018-04-05T15:01:39.724Z",
    name: "Veterans Discount",
    activationCode: null,
    locationName: "Location",
    locationId: "someLocationId",
    couponCode: null,
    active: true,
    customerType: "all",
    inventoryCategory: null,
    specialKey: "category",
    specialValue: null,
    group: "Veterans",
    timeStart: 0,
    timeEnd: 2359,
    daysOfWeek: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
    dateStart: null,
    dateEnd: null,
    percentOff: 10,
    dollarsOff: null,
    quantityThreshold: 1,
    quantityLimit: null,
    withLimits: false
  };
  const specialsListExpectation = eachLike(specialsGetExpectation);

  before(() =>
    provider.setup().then(opts => {
      // Get a dynamic port from the runtime
      process.env.API_HOST = `http://localhost:${opts.port}`;
    })
  );

  afterEach(() => provider.verify());

  describe("when I try to get the specials", () => {
    before(() => {
      provider.addInteraction(
        {
          uponReceiving: "a request to get a special",
          withRequest: {
            method: "GET",
            path: `/specials?clientId=${clientId}`
          },
          willRespondWith: {
            status: 200,
            headers: {
              "Content-Type": "application/json"
            },
            body: like(specialsGetExpectation)
          }
        }
      );
    });
    it("gets the special", () => {
      const ctx = {
        req: {
          user: {
            "https://flowhub.co/app_metadata": {
              clientId: "someClientId"
            }
          }
        }
      };
      getSpecials(ctx, () => {});
      expect(ctx.body).to.be.a("object");
    });
  });
});

after(() => {
  return provider.finalize();
});

This produces the following log file:

I, [2019-10-16T15:52:10.823916 #6605]  INFO -- : Registered expected interaction GET /specials?clientId=someClientId
D, [2019-10-16T15:52:10.824178 #6605] DEBUG -- : {
  "description": "a request to get a special",
  "request": {
    "method": "GET",
    "path": "/specials?clientId=someClientId"
  },
  "response": {
    "status": 200,
    "headers": {
      "Content-Type": "application/json"
    },
    "body": {
      "json_class": "Pact::SomethingLike",
      "contents": {
        "_id": "someId",
        "clientId": "someClientId",
        "createdAt": "2018-04-05T15:01:39.724Z",
        "updatedAt": "2018-04-05T15:01:39.724Z",
        "name": "Veterans Discount",
        "activationCode": null,
        "locationName": "Location",
        "locationId": "someLocationId",
        "couponCode": null,
        "active": true,
        "customerType": "all",
        "inventoryCategory": null,
        "specialKey": "category",
        "specialValue": null,
        "group": "Veterans",
        "timeStart": 0,
        "timeEnd": 2359,
        "daysOfWeek": [
          "Sun",
          "Mon",
          "Tue",
          "Wed",
          "Thu",
          "Fri",
          "Sat"
        ],
        "dateStart": null,
        "dateEnd": null,
        "percentOff": 10,
        "dollarsOff": null,
        "quantityThreshold": 1,
        "quantityLimit": null,
        "withLimits": false
      }
    }
  }
}
W, [2019-10-16T15:52:10.848063 #6605]  WARN -- : Verifying - actual interactions do not match expected interactions. 
Missing requests:
    GET /specials?clientId=someClientId


W, [2019-10-16T15:52:10.848112 #6605]  WARN -- : Missing requests:
    GET /specials?clientId=someClientId


I, [2019-10-16T15:52:10.855792 #6605]  INFO -- : Cleared interactions

I have created these tests with the guidance of the examples available here on GitHub, so I am not sure how the Promises are supposed to be handled. The structure of this test nearly identically follows the consumer example on your e2e example

I don't think the code there is waiting on the addInteraction promise - you will need to change:

    before(() => {
      provider.addInteraction(

to

before(() => provider.addInteraction(

(ie, returning the promise in the before block).

Also, is your getSpecials function passed/using the mock hostname? In the example you linked, the mock API is passed to the server using process.env.API_HOST.

Another comment - does your getSpecials(ctx, () => {}); code block at all? You'll need to wait for the network request to complete before finishing the test block. This is usually achieved by returning a promise. Something like:

    it("gets the special", async () => {
      const ctx = {
        req: {
          user: {
            "https://flowhub.co/app_metadata": {
              clientId: "someClientId"
            }
          }
        }
      };
      await getSpecials(ctx, () => {});
      expect(ctx.body).to.be.a("object");
    });

(this is achieved in the example by using to.eventually.be.fulfilled)

Thanks Tim, you beat me to it. It certainly has the smell of unhandled promises

That all makes sense, thanks so much for the help, guys. I am now back to a mostly working point with my tests. Curly braces were a major culprit and I just really needed to do some cleanup.

At this point, I am back to the original issue that I posted, where I need to set up a test with multiple Providers for each dependent microservice. Is there any documentation or examples you could point me toward for doing this?

EDIT
I got ahead of myself and answered my own questions about how to architect this whole test, so I only have one last question for you guys. In my test, the /products and /strains endpoint are dependent on only one microservice, so that was only one additional Provider I needed to write. However, since I don't want this Provider verified in this test (I don't want a Pact written for it), I only want one for the /specials endpoint and its associated Provider, how do I close down this Provider after these tests have run? I can use productsProvider.removeInteractions(); in the after hook but it only removes the interactions, but leaves the mock server up and running.

After getting my Provider-side verification working, I decided to look into this a little bit and I may have found a solution. In pact-js/httpPact.ts it looks like the shutting down of a Provider mock server is only handled in the provider.finalize() method. Specifically, the following block of code is what removes the mock server at lines 155-160:

.then(
        () =>
          new Promise<void>((resolve, reject) =>
            this.server.delete().then(() => resolve(), e => reject(e))
          )
      )

Could this method be abstracted into its own method i.e. tearDownMockService()?

Apologies, I didn't see your other comment.

However, since I don't want this Provider verified in this test (I don't want a Pact written for it)

Sorry I'm a little confused. If you don't want a Pact test for it, can you just _not_ call verify (or interact at all) on that provider after that test? Tearing own the mock server is the last thing you want to do with the provider, which is covered by a call to finalize anyway - so why can't you just call that and then leave it alone?

It would seem the issue is a structural/design issue with the tests.

Assuming I'm missing something though, one thing we could do is provide a handle to the underlying mockserver which would allow you to delete it yourself, or provide a tear down as discussed above. But I think there will be other unintended consequences we need to consider.

@mefellows The structure of the test is such that the Consumer is an API gateway and as part of a POST method, the gateway gathers data from a few endpoints that belong to another Provider. Right now, the test creates 2 providers, one for the POST endpoint and one for the second Provider that the gateway gathers data from. In my research on this problem, I saw a lot of people saying to do this, create 2 Providers.

So in my case, I have the Provider for my POST action (and all other interactions I am testing) and a second Provider that mocks out the data required for the POST action. In an after hook I call finalize on the first Provider which writes the pact and tears down the mockserver. The second Provider is never shut down though and I don't want a pact written for the interactions performed on it because that is not what I am testing here so I don't call finalize on it. This leaves the mockserver running on the specified port until I manually shut it down.

Maybe I am getting ahead of myself with the architecture of my tests, or maybe a better way to go about having that second provider would be to use a tool like nock?

Ah, right! OK this makes more sense. Thanks for explaining.

Yes, I wouldn't be using Pact for this. It's totally possible as you say/suggest, but I think it would be a little misleading / confusing using it in this way. But it's an interesting thought.

For now, I would use Nock - it's faster (in-memory) but also people know it is just a local stub without the additional behaviour Pact has.

Just an idea, but perhaps we could create a pact.addStub() method which is basically the same as the pact.addInteraction() method, but is not validated. I think that would be nicer, because it would be completely managed (i.e. you wouldn't need to manage a mock server lifecycle).

How does all of that sound?

@mefellows That all sounds good to me. I switched the second provider over to Nock and tests are running faster and are easier to maintain and definitely look nicer. It may be nice to be able to stub within Pact itself, but in my specific case I don't think it is too bad to use other libraries in the JavaScript ecosystem.

Thanks for all the help!

I think for now, let's stick with Nock and see if other related use cases come up. Thanks for raising this, hopefully it helps somebody in the future. I'm going to close this off for now - looks like you're up and running.

Was this page helpful?
0 / 5 - 0 ratings