Mock Network When Using Next.js getServerSideProps Call

How to run Next.js inside the Cypress plugin process to be able to stub network call made by the getServerSideProps call.

Cypress has a great way to spy or stub network calls the application makes. Just use the cy.intercept command and have the full control over Ajax calls and static resources. But sometimes, the application is making the network calls from the server-side call. For example, a Next.js application might use the getServerSideProps method to retrieve a joke to be shown to the user.

pages/index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import styles from './index.module.css'

function HomePage({ joke }) {
return <div className={styles.home}>
<div data-cy="joke" className={styles.content}>{joke}</div>
</div>
}

export async function getServerSideProps(context) {
console.log('getServerSideProps')

const url = 'https://icanhazdadjoke.com/'
const res = await fetch(url, {
headers: {
'Accept': 'application/json'
}
})
const data = await res.json()
console.log(data)

return {
props: {
joke: data.joke
},
}
}

export default HomePage

Without any network mocking, the Cypress test can only assert that there is some text on the page.

cypress/integration/spec.js
1
2
3
4
5
6
/// <reference types="cypress" />

it('fetches a random joke', () => {
cy.visit('/')
cy.get('[data-cy=joke]').should('not.be.empty')
})

A random joke is displayed

It would be very nice to stub the fetch call used by the getServerSideProps method. Unfortunately, this call is not made from the web application running in the browser; it is made from the server process.

🎁 You can find the source code from this blog post in the repo bahmutov/nock-getServerSideProps. You can also watch the explanation in this video Stub Network Calls Made by Next.js App in getServerSideProps Method.

What if we had access to the server process so we could install network stubs whenever we needed? We could use nock library to control the network - the same network used by the Next.js server-side process.

Here is what we can do - we could run the Next.js server right inside the Cypress plugins process. This process runs in the background, and the test running in the browser can communicate with the process through the cy.task command. We can start the Next.js application through the custom http server approach.

cypress/plugins/index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const http = require('http')
const next = require('next')

// start the Next.js server when Cypress starts
module.exports = async (on, config) => {
const app = next({ dev: true })
const handleNextRequests = app.getRequestHandler()
await app.prepare()

const customServer = new http.Server(async (req, res) => {
return handleNextRequests(req, res)
})

await new Promise((resolve, reject) => {
customServer.listen(3000, (err) => {
if (err) {
return reject(err)
}
console.log('> Ready on http://localhost:3000')
resolve()
})
})

return config
}

🤔 Wait, doesn't Cypress documentation advise not to start the server from the plugin process? Yes it does, but the documentation cannot stop us. We are like a professional driver on the closed track - we can do whatever we want.

We start the server inside the Cypress process instead of an external process. This only works locally, of course. Let's add network mocking using nock. We need a way to reset the network mocks (we still want the first test to happen without stubbing), and we need a way to set a specific network mock. We can create tasks for these.

cypress/plugins/index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const nock = require('nock')
...

// start the Next.js server when Cypress starts
module.exports = async (on, config) => {
...

// register handlers for cy.task command
// https://on.cypress.io/task
on('task', {
clearNock() {
nock.restore()
nock.cleanAll()

return null
},

async nock({ hostname, method, path, statusCode, body }) {
nock.activate()

console.log('nock will: %s %s%s respond with %d %o',
method, hostname, path, statusCode, body)

// add one-time network stub like
// nock('https://icanhazdadjoke.com').get('/').reply(200, ...)
method = method.toLowerCase()
nock(hostname)[method](path).reply(statusCode, body)

return null
},
})

return config
}

Let's write the spec.

cypress/integration/spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/// <reference types="cypress" />

beforeEach(() => {
cy.task('clearNock')
})

it('fetches a random joke', () => {
cy.visit('/')
cy.get('[data-cy=joke]').should('not.be.empty')
})

it('getServerSideProps returns mock', () => {
const joke = 'Our wedding was so beautiful, even the cake was in tiers.'
cy.task('nock', {
hostname: 'https://icanhazdadjoke.com',
method: 'GET',
path: '/',
statusCode: 200,
body: {
id: 'NmbFtH69hFd',
joke,
status: 200
}
})
cy.visit('/')
// nock has worked!
cy.contains('[data-cy=joke]', joke)
})

The browser shows our joke! The network stub has worked.

The network stub has worked

We can check the terminal output to confirm the getServerSideProps got the joke we have set up.

The terminal output shows mocking messages

Our Cypress test and the Next.js app running inside the plugins process are shown in the diagram below.

The server running inside the plugins process with network mocked by the nock library

See also

  • if you can run your Next.js server inside the browser using StackBlitz, then you can stub the network calls from the server using cy.intercept command as the video below shows.