javascripttestingcypresspactcontract

Cannot make Cypress and Pact work together


I already have working project with few passing Cypress tests. Now I'm trying to add contract tests using Cypress + Pact

In developer console I can see that app is calling /api/v1/document-service, but I get:

Pact verification failed - expected interactions did not match actual.

Part of logs:

W, [2021-07-20T12:49:37.157389 #34805]  WARN -- : Verifying - actual interactions do not match expected interactions.

Missing requests:
    POST /api/v1/document-service

W, [2021-07-20T12:49:37.157489 #34805]  WARN -- : Missing requests:
    POST /api/v1/document-service

I'm using:

  cypress: 7.5.0
  @pact-foundation/pact: 9.16.0

Steps I've done:

  1. Added cypress plugin (https://github.com/pactflow/example-consumer-cypress/blob/master/cypress/plugins/cypress-pact.js)

  2. Added commands (https://github.com/pactflow/example-consumer-cypress/blob/master/cypress/support/commands.js)

  3. Added config to cypress.json (https://github.com/pactflow/example-consumer-cypress/blob/master/cypress.json) - not sure what to put to baseUrl, if I don't want interactions with real server.

  4. Added test:

    let server;
    
    describe('Simple', () => {
        before(() => {
            cy.mockServer({
                consumer: 'example-cypress-consumer',
                provider: 'pactflow-example-provider',
            }).then(opts => {
                cy.log(opts)
                server = opts
            })
        });
    
        beforeEach(() => {
            cy.fakeLogin()
    
            cy.addMockRoute({
                server,
                as: 'products',
                state: 'products exist',
                uponReceiving: 'a request to all products',
                withRequest: {
                    method: 'POST',
                    path: '/api/v1/document-service',
                },
                willRespondWith: {
                    status: 200,
                    body: {
                        data: {
                            collections: [
                                {
                                    id: '954',
                                    name: 'paystubs',
                                },
                                {
                                    id: '1607',
                                    name: 'mystubs',
                                },
                            ],
                        },
                    },
                },
            });
        });
        it('is ok?', () => {
            cy.visit('/new/experiments/FirstProject/collections');
        });
    })
    

Tried both using deprecated cy.server()/cy.route() and new cy.intercept(), but still verification failures.


Solution

  • At Pactflow, we've spent about 6 months using Cypress and Pact together, using the Pact mock service to generate pacts from our Cypress tests as suggested in https://pactflow.io/blog/cypress-pact-front-end-testing-with-confidence/

    We have this week decided to change our approach, for the following reasons.

    1. Our Cypress tests are much slower than our unit tests (15+ minutes), so generating the pact, and then getting it verified takes a lot longer when we generate the pact from our Cypress tests vs our unit tests.
    2. Our Cypress tests can fail for reasons that are not to do with Pact (eg. layout changes, issues with the memory consumption on the build nodes, UI library upgrades etc) and this stops the pact from being generated.
    3. The Cypress intercept() and mock service don't play very nicely together. Any request that does not have an explicit intercept() for Cypress ends up at the mock service, and every time a new endpoint is used by the page, the request hits the mock service, which then fails the overall test, even if that endpoint was not required for that specific test.
    4. When a Cypress test fails, there is very good debugging provided by the Cypress library. When it fails for reasons to do with Pact, it's currently hard to identify the cause, because that information is not displayed in the browser (this could potentially be improved, however, it won't mitigate the already listed issues).

    Our new approach is to generate our Pacts from our unit tests (instead of our Cypress tests) and to share the fixtures between the unit and Cypress tests, using a function that strips of the Pact matchers. eg.

    const SOME_INTERACTION = {
      state: 'some state',
      uponReceiving: "some request",
      withRequest: {
        method: "GET",
        path: "/something"
      },
      willRespondWith: {
        status: 200,
        body: {
          something: like("hello")
        }
      }
    }
    
    

    Unit test

    describe('Something', () => {
      let someProviderMockService
      beforeAll(() => {
        someProviderMockService = new Pact({
          consumer: 'some-consumer',
          provider: 'some-provider'
        })
        return someProviderMockService.setup()
      })
      afterAll(() => someProviderMockService.finalize())
      afterEach(() => someProviderMockService.verify())
    
      describe('loadSomething', () => {
        beforeEach(() => {
          return someProviderMockService.addInteraction(SOME_INTERACTION)
        })
    
        it('returns something', async () => {
          const something = await loadSomething()
          //expectations here
        })
      })
    })
    
    

    Function that turns the Pact interaction format into Cypress route format, and strips out the Pact matchers.

    const Matchers = require('@pact-foundation/pact-web').Matchers
    
    // TODO map the request body and query parameters too
    const toCypressInteraction = (interaction) => {
      return {
        method: interaction.withRequest.method,
        url: Matchers.extractPayload(interaction.withRequest.path),
        status: interaction.willRespondWith.status,
        headers: Matchers.extractPayload(interaction.willRespondWith.headers),
        response: Matchers.extractPayload(interaction.willRespondWith.body)
      }
    }
    

    In the Cypress test

    cy.route(toCypressInteraction(SOME_INTERACTION))
    

    This approach has the following benefits:

    1. The pact is generated quickly.
    2. The pact is generated reliably.
    3. The Cypress tests are more reliable and easier to debug.
    4. The requests used in the Cypress tests are verified to be correct.
    5. There is less chance of "interaction bloat", where interactions are added merely to test UI features, rather than because they provide valuable coverage.

    I hope this information is helpful for you. We now recommend this approach over direct use of the mock service in Cypress tests.