Introduction
As REST APIs grow in complexity, comprehensive end-to-end testing becomes essential to prevent regressions. Cypress is a popular front-end testing tool that also works great for testing backend APIs.
In this guide, we’ll learn how to test Express.js REST APIs from end-to-end using Cypress – from basic routing to authentication and database verification. We’ll see how Cypress allows writing entire sequences of API tests easily.
By the end, you’ll be able to incorporate automated API testing into your Express development workflow using Cypress. Let’s get started!
Introduction to Cypress
Cypress is a front-end testing framework that runs in the browser for fast, reliable testing of web applications.
Some key capabilities:
- Easy debugging with readable errors and screenshots
- Time travel between test states
- Spies, stubs and clocks for control
- Interactive GUI runner
- Automatic waiting and retries
- Screenshots and videos of runs
Cypress also provides a cy.request()
method that allows making arbitrary HTTP requests for testing. This means we can leverage Cypress for testing backend APIs as well!
We’ll focus specifically on API testing in this guide. Let’s look at how to add Cypress to an Express project.
Setting Up Cypress in Express
Ensure Node.js is installed first. Then scaffold a new Express app:
npm init -y
npm install express
Add a simple HTTP endpoint in app.js
:
const express = require('express');
const app = express();
app.get('/users', (req, res) => {
res.status(200).json([{ id: 1 }]);
});
app.listen(3000);
This basic Express server responds to GET /users
with some data.
To add Cypress, install it:
npm install cypress --save-dev
Cypress gets added as a dev dependency. Next, open Cypress and generate sample tests:
npx cypress open
This launches the Cypress Test Runner. Click on ‘Create your first test’ to scaffold sample tests in cypress/integration/
.
We are now ready to start writing API tests!
Testing Routes and Responses
Let’s start with a simple test for our /users
route:
// cypress/integration/users.spec.js
describe('Users API', () => {
it('gets users', () => {
cy.request({
method: 'GET',
url: 'http://localhost:3000/users'
}).should((response) => {
expect(response.status).to.eq(200)
expect(response.body).to.have.length(1)
})
})
})
Here we:
- Use
cy.request()
to make a GET call to /users - Assert the response status code is 200
- Assert the response body contains 1 user
This validates our /users
endpoint returns the expected data.
To run the test:
npm run dev # start server
npx cypress open # launch runner
# Run users.spec.js test
We can continue adding more tests for additional routes and edge cases. Cypress makes writing full pathway tests easy.
Organizing with Fixtures and Params
For better organization, we can externalize test data into fixtures and reuse them across tests.
Example fixture file:
// cypress/fixtures/users.json
[
{"id": 1, "name": "John"},
{"id": 2, "name": "Jane"}
]
In our test, we can load it:
it('gets users', () => {
cy.fixture('users').then(users => {
// ...
})
})
The test can now use the users
fixture data.
For dynamic testing, we can pass test parameters from a file:
// cypress/plugins/index.js
module.exports = (on) => {
on('task', {
getDataFromFile(file) {
// return fixture data for use
}
})
}
And in tests:
it('tests users', () => {
cy.task('getDataFromFile', 'users.json').then(users => {
// test using users
})
})
This allows parameterized, data-driven testing scenarios.
Mocking Responses
Cypress allows stubbing responses to test different scenarios:
cy.intercept('GET', '/users', {
statusCode: 200,
body: [{id: 1}]
}).as('getUsers')
cy.get('@getUsers').its('response.statusCode').should('eq', 200)
Here we:
- Stub the /users GET route to return a mock response
- Alias the route as ‘getUsers’
- Assert the stubbed response status code
Stub responses allow testing various scenarios and edge cases.
Making Requests
We can make arbitrary API requests using cy.request()
:
cy.request({
method: 'POST',
url: '/users',
body: {
"name": "John"
}
}).its('body').should('deep.eq', {
id: 1,
name: "John"
})
This covers how to make POST, PUT and other CRUD requests in tests.
The .its()
command allows accessing properties for assertions.
Authentication and Sessions
Cypress allows logging in and testing authenticated states:
// Login
cy.request({
method: 'POST',
url: '/login',
body: {
username: 'john@example.com',
password: 'password123'
}
}).then(({ body }) => {
const { accessToken } = body
// Set token for subsequent requests
Cypress.env({ accessToken })
})
// Authenticated request
cy.request({
headers: {
Authorization: `Bearer ${Cypress.env('accessToken')}`
},
url: '/private'
})
This posts a login request, saves the returned token, and uses it in later requests by setting Cypress.env()
.
We can test the entire auth flow – from login to authorized actions.
Database Validation
To test persistence, we need to verify database state from our tests.
If using MongoDB, we can directly connect and run assertions:
// Connect
cy.task('connectDB', { connectionString: <url> })
cy.request({
method: 'POST',
url: '/users',
body: { /* user data */ }
})
// Query database
cy.task('findUserInDB', { email: 'jane@example.com' })
.then(user => {
expect(user).to.exist
})
Here we connect to the database before tests, make a request, and verify the user was inserted by querying directly.
Similar techniques work for other databases like MySQL, Postgres etc. This validates the full request handling.
Test Reporters and Parallelization
For CI environments, Cypress can generate JUnit XML test reports:
cypress run --reporter junit --reporter-options "mochaFile=results/my-test-output.xml"
We can also run tests in parallel to reduce time:
cypress run --parallel --record
This distributes tests across CPU cores. See docs for details.
Recap
Let’s recap what we covered for testing Express APIs with Cypress:
- Installing Cypress in an Express project
- Writing route handler tests
- Organizing tests with fixtures and parameters
- Mocking stub responses
- Making arbitrary API requests
- Testing authentication and sessions
- Verifying persistence by checking database
- Generating CI test reports
- Running tests in parallel
These techniques allow comprehensive API testing without spinng up a separate frontend. Cypress provides a powerful node-based testing solution for REST APIs.
Conclusion
Through expressive, declarative syntax and built-in tools like response stubbing, Cypress makes end-to-end API testing straightforward.
We saw how to install Cypress for testing Express.js apps, validate responses, mock data, check persistence, and handle CI integration.
Cypress eliminates the need for separate integration test suites by testing right from the frontend framework. The interactive runner combined with automatic waits and retries makes writing reliable tests simple.
Adding Cypress gives you the confidence that your APIs behave as intended from end-to-end. The entire external interface of your app can be tested without mock servers.
Cypress offers much more in terms of testing React/Vue apps, component testing, visual regression testing, and more. Consider integrating it into your JavaScript projects to enable both frontend and backend testing. Let me know if you have any other questions!
Frequently Asked Questions
Is Cypress only for testing front-end code?
No, Cypress can test any backend HTTP APIs and services by directly making HTTP requests. It is not limited to only front-end DOM manipulation.
The cy.request()
command allows sending arbitrary HTTP requests to test endpoints and validate responses.
What are some advantages of Cypress over Postman?
Some key advantages Cypress has over Postman:
- Tests are written in code rather than GUI, allowing for scripts and assertions
- Tests can be run end-to-end without requiring call order
- Tests can interact with front-end and back-end together
- Includes built-in tools like spies, stubs, clocks
- Automatic waiting, retrying reduces flakiness
So Cypress testing is more automated, flexible and robust compared to Postman.
Is there a way to share data across tests in Cypress?
Some ways to share data across tests:
- Store data in Cypress.env() object
- Write data to disk using cy.writeFile()
- Save data to browser localStorage
- Define data in external JSON fixture files
This allows reusing authorization tokens, test data etc. across multiple specs and runs.
Can we run Cypress tests in parallel?
Yes, Cypress supports parallelization out of the box. Use the –parallel flag:
cypress run --parallel
This auto-balances tests across CPU cores. Parallelization speeds up test runs significantly.
Is there a way to detect API changes using Cypress?
Cypress has a responseDiff plugin that can fail tests on response changes:
cy.responseDiff({
status: 200,
body: []
})
Snapshots also capture responses to visually diff changes.
These features detect accidental API breaks automatically.