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.
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.
returnnewPromise((resolve) => { const server = app.listen(port, function () { const port = server.address().port console.log('Example app listening at port %d', port)
// close the server constclose = () => { returnnewPromise((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 returnclose() })
The first tests
Let's write a Cypress API test that confirms something, maybe some fields in the response body.
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.
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 = awaitmakeServer() // 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 } awaitclose() 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) }) })
$ 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.
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 = awaitmakeServer() // 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 } awaitclose() 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) }) })
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