Server Running Inside Cypress Plugin Process

How to run and restart the Express server inside the Cypress plugin process.

In the blog post How to correctly unit test Express server I have shown how to unit test an Express server using Mocha. In this blog post I will show how to run the Express server inside Cypress plugin process, and how to restart it before each spec.

🎁 You can find the source code in the repo bahmutov/server-restart-example.

The server

My server is a plain Express server that I can construct and close when needed. Here is the source code - the server really has a single endpoint, that is enough for us to show the tests.

src/server.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
const express = require('express')
function makeServer() {
const app = express()
app.get('/', function (req, res) {
res.json({
message: 'Hello World!',
responseId: Math.round(Math.random() * 1e6),
port,
})
})

const port = 6000

return new Promise((resolve) => {
const server = app.listen(port, function () {
const port = server.address().port
console.log('Example app listening at port %d', port)

// close the server
const close = () => {
return new Promise((resolve) => {
console.log('closing server')
server.close(resolve)
})
}

resolve({ server, port, close })
})
})
}

module.exports = makeServer

To construct and shut down the server one needs to use the returned object.

app.js
1
2
3
4
5
const makeServer = require('./server')
makeServer().then(({server, port, close}) => {
// wait for some signal, then shutdown
return close()
})

The first tests

Let's write a Cypress API test that confirms something, maybe some fields in the response body.

cypress/integration/spec1.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
describe('Express server 1', () => {
it('responds', () => {
cy.request('http://localhost:6000').its('body').should('deep.include', {
message: 'Hello World!',
port: 6000,
})
})

it('responds with random id', () => {
cy.request('http://localhost:6000')
.its('body.responseId')
.should('be.a', 'number')
.and('be.within', 1e5, 1e6)
})
})

Ok, but before we run the tests we need to start the server. We could use start-server-and-test, but let's just run the server inside the Cypress plugin process, which runs in Node. We can use the before:spec event fired by Cypress before every spec starts to make sure the server is up and running. After the spec file finishes running all tests, we can close the server.

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
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config

let server, port, close

on('before:spec', async (spec) => {
// we can customize the server based on the spec about to run
const info = await makeServer()
// save the server instance information
server = info.server
port = info.port
close = info.close
console.log('started the server on port %d', port)
})

on('after:spec', async (spec) => {
if (!server) {
console.log('no server to close')
return
}
await close()
console.log('closed the server running on port %d', port)
})
}

Important: the before:spec and after:spec are only fired in the non-interactive mode when you use cypress run. We need to enable them to run in the interactive mode too. In Cypress v9 we should enable this using the experimental feature experimentalInteractiveRunEvents.

cypress.json
1
2
3
{
"experimentalInteractiveRunEvents": true
}

While we are at it, let's add another spec file

cypress/integration/spec2.js
1
2
3
4
5
6
7
8
describe('Express server 2', () => {
it('has timing info', () => {
cy.request('http://localhost:6000')
// Cypress adds duration ms to the response
.its('duration')
.should('be.above', 0)
})
})

Let's see our tests and the server in action.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ npx cypress run
...
Browser: Electron 94 (headless)
Node Version: v14.17.1
...
Running: spec1.js
Example app listening at port 6000
started the server on port 6000
...
Express server 1
✓ responds (60ms)
✓ responds with random id (21ms)
...
closing server
closed the server running on port 6000
...
Running: spec2.js
Example app listening at port 6000
started the server on port 6000
...
closing server
closed the server running on port 6000

Cypress v9 uses the default system Node, making it simple to install the dependencies and run the server. Every spec starts the server and shuts it down.

Random port

Let's pretend that each spec starts the server a little bit differently. For example, what if we need to start the server at a random port? How would we send the port number to the spec file so it makes the right request? In our example, we can use get-port module to find an open port to use.

src/server.js
1
2
3
4
5
6
7
8
9
10
11
12
const getPort = require('get-port')
const { makeRange } = getPort
...
// the port value will be set later
let port
// random number between 6100 and 6300
const n = Math.round(Math.random() * 200) + 6100
const ports = makeRange(n, 6300)
getPort({ port: ports }).then(p => {
port = p
// start the server at the port number
})

Great, now let's update the tests

Sending the info to the spec

We can start the server and save the port, but we need to somehow tell the spec which port to use. The port number is stored in the plugin memory process as a local variable. To let the spec know, we can run cy.task and fetch the port number.

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
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config

let server, port, close

on('before:spec', async (spec) => {
// we can customize the server based on the spec about to run
const info = await makeServer()
// save the server instance information
server = info.server
port = info.port
close = info.close
console.log('started the server on port %d', port)
})

on('after:spec', async (spec) => {
if (!server) {
console.log('no server to close')
return
}
await close()
console.log('closed the server running on port %d', port)
})

on('task', {
getPort() {
// cy.task cannot return undefined
// thus we return null if there is no port value
return port || null
},
})
}

We want every spec to "know" the port number. Thus we can call the cy.task('getPort') from the support file which runs before every spec file. We can store the returned port number in the Cypress.env object.

cypress/support/index.js
1
2
3
4
5
6
7
8
before(() => {
cy.task('getPort').then((port) => {
expect(port).to.be.a('number')
// store the port and url in the Cypress env object
Cypress.env('port', port)
Cypress.env('url', `http://localhost:${port}`)
})
})

Let's modify the spec to use the port. Important the values of port and url are set in the before hook, thus they are going to be set inside the test or any hooks. Thus we need to get the value from the Cypress.env object in the test for example:

1
2
3
4
5
6
7
8
9
10
describe('Express server 2', () => {
it('has timing info', () => {
const port = Cypress.env('port')
expect(port).to.be.a('number').and.to.be.within(6100, 6300)
cy.request(`http://localhost:${port}`)
// Cypress adds duration ms to the response
.its('duration')
.should('be.above', 0)
})
})

Super, everything works.

The test uses the random port number from the plugin file

Similar update in the spec1.js

cypress/integration/spec1.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
describe('Express server 1', () => {
it('responds', () => {
const url = Cypress.env('url')
const port = Cypress.env('port')
cy.request(url).its('body').should('deep.include', {
message: 'Hello World!',
port,
})
})

it('responds with random id', () => {
const url = Cypress.env('url')
cy.request(url)
.its('body.responseId')
.should('be.a', 'number')
.and('be.within', 1e5, 1e6)
})
})

Want more API testing goodness? Try using cy-api plugin.

Use cy-spok

Finally, any time we need to do network assertions, and using the cy-spok as a very convenient way of writing complex object assertions. Let's install cy-spok

1
2
$ npm i -D cy-spok
+ [email protected]

In the spec file, let's use the spok instead of deep.include, and we can thus verify the random response ID property.

cypress/integration/spec1.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import spok from 'cy-spok'

describe('Express server 1', () => {
it('responds', () => {
const url = Cypress.env('url')
const port = Cypress.env('port')
cy.request(url)
.its('body')
.should(
spok({
message: 'Hello World!',
port,
responseId: spok.range(1e5, 1e6),
}),
)
})
})

We just replaced two tests with a single test, and the Command Log is cleaner too.

Assert object properties using cy-spok

Beautiful.