Mocking downstream dependencies with Mockttp in Cypress tests for NextJS apps

23 Aug 2023

In a complex or distributed system, you might have an app with multiple downstream dependencies. End-to-end tests might not be an option, or they can serve as another level of testing in addition to your Cypress and Jest tests. It is possible to mock downstream dependencies for calls from your BFF using Mockttp. This post briefly explains how to set this up.

The author assumes you either already have a NextJS app with Cypress or can set one up. Below is a short recap, just in case. If you are not interested in the recap, feel free to skip it.

npx create-next-app@latest --use-yarn # using yarn is just a preference
cd <your_app_name>
yarn add -D cypress
yarn cypress open

In the Cypress UI, you can follow the wizard to bootstrap the files that Cypress needs.

Let's add an external API call on the server side when rendering the home page (src/app/page.js or another location, depending on your choices during the app setup).

.env.local:

API=https://jsonplaceholder.typicode.com

src/app/page.js:

async function getData() {
  const res = await fetch(`${process.env.API}/todos/1`)
  if (!res.ok) {
    throw new Error('Failed to fetch data')
  }
  return res.json()
}

...


export default async function Home() {
  const data = await getData()
  ...
  return (
    ...
    <p>UserID: {data.userId}</p>
    ...
  )
}

Add a simple Cypress test:

it('renders the JSON', () => {
  cy.visit('/')
  cy.contains('UserID: 1').should('be.visible')
})

(Adjust the baseUrl values for both Cypress and NextJS as necessary.)

Adding Mockttp to the project

yarn add -D mockttp

The Mockttp CLI starts a standalone server and passes control to the command you provide. This goes to the scripts of the package.json:

    "cypress:open": "mockttp -c \"cypress open .\""

Making Mockttp work in Cypress tests

Now, if you look at the Cypress UI, you might see an error related to how Webpack treats WebAssembly modules:

Error: Webpack Compilation Error
Module parse failed: Unexpected character '' (1:0)
The module seem to be a WebAssembly module, but module is not flagged as WebAssembly module for webpack.
BREAKING CHANGE: Since webpack 5 WebAssembly is not enabled by default and flagged as experimental feature.

We can fix it by adding a @cypress/webpack-preprocessor (and its peer dependencies) and overriding the webpack config:

yarn add -D @cypress/webpack-preprocessor webpack @babel/core @babel/preset-env babel-loader buffer stream-browserify

cypress.config.js:

const { defineConfig } = require("cypress");
const webpackPreprocessor = require("@cypress/webpack-preprocessor");
const webpack = require("webpack");

...

setupNodeEvents(on, config) {
  const webpackOptions = {
    resolve: {
      fallback: {
        zlib: false,
        fs: false,
        path: false,
        querystring: false,
        url: require.resolve("url"),
        stream: require.resolve("stream-browserify"),
        buffer: require.resolve("buffer"),
      },
    },
    experiments: {
      asyncWebAssembly: true,
    },
    plugins: [
      new webpack.ProvidePlugin({ process: "process/browser" }),
      new webpack.ProvidePlugin({
        Buffer: ["buffer", "Buffer"],
      })
    ]
  }
  on('file:preprocessor', webpackPreprocessor({ webpackOptions }))
  return config
}

(you'll need to restart the yarn cypress:open command)

Using the mock server in tests

Before supplying our mocks, we must direct the app to use the mock server instead of the actual downstream dependency. We did it here by supplying a substitute value inside the dev yarn in scripts in package.json, but you can absolutely do it via a dedicated .env file, CI variable overrides or any other way.

"dev": "API=http://localhost:9999/jsonplaceholder next dev -p 8080",
"start": "API=http://localhost:9999/jsonplaceholder next start -p 8080"

After doing this and restarting the yarn dev command, we expect the tests to fail since there's no server on localhost:9999 yet.

We used a subpath (/jsonplaceholder) on the server. This way, it's possible to use one instance of Mockttp to mock several upstream backends under different subpaths.

Now we can update our spec file so that it connects to the Mockttp server via getRemote and passes on the mocks:

import { getRemote } from 'mockttp'

const server = getRemote()

before(() => {
  cy.wrap(server.start(9999))
})

after(() => {
  cy.wrap(server.stop())
})

it('renders the JSON', () => {
  cy.wrap(server.forGet("/jsonplaceholder/todos/1").always().thenJson(200, { userId: 2 }))
  cy.visit('/')
  cy.contains('UserID: 2').should('be.visible')
})

It's possible to get the port dynamically from server.start(). We don't do it here because we want to provide the specific mock URL (including the port) as part of our imaginary CI configuration.

On an additional note, while running a dev server of your app locally to avoid any issues with NextJS caching, we found it helpful to turn off NextJS's caching locally:

const res = await fetch(`${process.env.API}/todos/1`, { cache: 'no-store' })

Testing the requests the BFF sends

You can also assert the requests that the NextJS server-side sends to the downstream dependencies. Below is an example. For this to work, we also need to change before and after to beforeEach and afterEach to ensure mocks are isolated between the tests.

it('sends a correct request', () => {
  cy.wrap(server.forGet("/jsonplaceholder/todos/1").always().thenJson(200, { userId: 2 }))
    .as('mockEndpoint')
  cy.visit('/')
  cy.get('@mockEndpoint').then(mockEndpoint => {
    cy.wrap(mockEndpoint?.getSeenRequests()).then(reqs => {
      cy.wrap(reqs).should('have.length', 1)
      cy.wrap(reqs[0]).its('headers').should('have.property', 'accept')
    })
  })
})

About

This blog contains things around software engineering that I consider worth sharing or at least noting down. I will be happy if you find any of these notes helpful. Always feel free to give me feedback at: kk [at] kirill-k.pro.

KK © 2025