Cypress is a popular testing framework tailored for JavaScript applications. While it is primarily designed to test UI components and interactions with UI elements in a browser, it is also well-suited for testing APIs. You can use the framework to test RESTful APIs via HTTP requests and validate the responses.
Cypress lets you write comprehensive tests that span the full spectrum of your web application's workflow.
Getting Started With API Testing Using Cypress
Cypress helps you verify that your APIs work as you expect them to. This process typically includes testing the API's endpoints, input data, and HTTP responses. You can verify integration with any external services, and confirm that error-handling mechanisms work correctly.
Testing your APIs ensures they are functional, reliable, and meet the needs of apps that depend on them. It helps to identify and fix bugs early on, preventing problems from occurring in production.
Cypress is a great UI testing tool, used by some of the popular JavaScript frameworks. Its ability to make and test HTTP requests makes it equally effective at testing APIs.
It does this by using Node.js as its engine to make HTTP requests and handle their responses.
You can find this project's code in its GitHub repository.
Create an Express.js REST API
To get started, create an Express web server, and install this package in your project:
npm install cors
Next, add the Cypress package to your project:
npm install cypress --save-dev
Finally, update your package.json file to include this test script:
"test": "npx cypress open"
Define the API Controllers
In a real-world case, you’d make API calls to read and write data from a database or an external API. However, for this example, you'll simulate and test such API calls by adding and fetching user data from an array.
In the root directory of your project folder, create a controllers/userControllers.js file, and add the following code.
First, define a registerUser controller function that will manage the user registration route. It will extract the user's data from the request body, create a new user object, and add it to the users array. If the process is successful, it should respond with a 201 status code and a message indicating that it has registered the user.
const users = [];
exports.registerUser = async (req, res) => {
const { username, password } = req.body;
try {
const newUser = { username, password };
users.push(newUser);
res.status(201).send({ message: 'User registered successfully' });
} catch (error) {
console.error(error);
res.status(500).send({ message: 'An error occurred!!' });
}
};
Add a second function—getUsers—to retrieve user data from the array, and return it as JSON response.
exports.getUsers = async (req, res) => {
try {
res.json(users);
} catch (error) {
console.error(error);
res.status(500).send({ message: 'An error occurred!!' });
}
};
Lastly, you can also simulate login attempts. In the same file, add this code to check if the given username and password match any user data in the users array:
exports.loginUser = async (req, res) => {
const { username, password } = req.body;
try {
const user = users.find((u) =>
u.username === username && u.password === password);
if (user) {
res.status(200).send({ message: 'Login successful' });
} else {
res.status(401).send({ message: 'Invalid credentials' });
}
} catch (error) {
console.error(error);
res.status(500).send({ message: 'An error occurred!!' });
}
};
Define the API Routes
To define the routes for your Express REST API, create a new routes/userRoutes.js file in the root directory, and add this code to it:
const express = require('express');
const router = express.Router();
const userControllers = require('../controllers/userControllers');
const baseURL = '/v1/api/';
router.post(baseURL + 'register', userControllers.registerUser);
router.get(baseURL + 'users', userControllers.getUsers);
router.post(baseURL + 'login', userControllers.loginUser);
module.exports = router;
Update the Server.js File
Update the server.js file to configure the API as follows:
const express = require('express');
const cors = require('cors');
const app = express();
const port = 5000;
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cors());
const userRoutes = require('./routes/userRoutes');
app.use('/', userRoutes);
app.listen(port, () => {
console.log(`Server is listening at http://localhost:${port}`);
});
module.exports = app;
Set Up the Test Environment
With the demo API in place, you’re ready to set up the testing environment. Start the development server with this terminal command:
node server.js
Next, run the test script command in a separate terminal:
npm run test
This command will launch the Cypress desktop client, which provides the testing environment. Once it's open, click the E2E Testing button. End-to-end tests ensure that you test the Express API as a whole, meaning Cypress will have access to the web server, the routes, and the associated controller functions.
Next, click Continue to add Cypress configuration files.
Once the setup process is complete, you should see a new Cypress folder in your project. Cypress will also add a cypress.config.js file which contains the configuration settings for your tests.
Go ahead and update this file to include your server base URL as follows:
const { defineConfig } = require("cypress");
module.exports = defineConfig({
chromeWebSecurity: false,
e2e: {
baseUrl: 'http://localhost:5000',
setupNodeEvents(on, config) {
},
},
});
Write the Test Cases
Now you’re ready to write some test cases. First, select the browser in which Cypress will launch to run the tests from the options available on the Cypress client.
Next, click the Create new spec button to create your test file, and provide a name. Then click Create spec.
Now, open the cypress/fixtures/example.json file, and update its contents with the following user credentials. Fixtures are files that contain static test data that you can use in the test cases.
{
"username": "testuser",
"password": "password123"
}
Cypress provides a cy.request method to make HTTP requests to a web server. You can use it to test different types of HTTP endpoints that manage different operations including GET, POST, PUT, and DELETE.
To test the three API routes you defined earlier, start by describing the test case for the register endpoint. This test case should verify that the endpoint is working correctly by successfully registering a new user and validating the assertions.
Open the cypress/e2e/user.routes.spec.cy.js file and update its contents with the following code.
describe('User Routes', () => {
it('registers a new user', () => {
cy.fixture('example').then((testUser) => {
cy.request({
method: 'POST',
url: `${baseUrl}/v1/api/register`,
body: testUser,
}).then((response) => {
expect(response.status).to.eq(201);
expect(response.body.message).to.eq('User registered successfully');
});
});
});
In this test, Cypress will load the test data in the fixture file, and make POST requests to the specified endpoint with the data in the request body. If all the assertions pass, the test case will pass. Otherwise, it will fail.
It's worth noting that the syntax for Cypress tests closely resembles the syntax used in Mocha tests, which Cypress has adopted.
Now, describe the test for the users route. The test should verify that the response contains user data when requests are made to this endpoint. To achieve this, add the following code within the describe test block.
it('gets users data and the username matches test data', () => {
cy.fixture('example').then((expectedUserData) => {
cy.request({
method: 'GET',
url: `${baseUrl}/v1/api/users`,
}).then((response) => {
expect(response.status).to.eq(200);
const username = response.body[0].username;
expect(username).to.eq(expectedUserData.username);
});
});
});
Finally, include a test case that will test the login endpoint and assert that the response status is 200, indicating a successful login attempt.
it('logs in a user', () => {
cy.fixture('example').then((loginData) => {
cy.request({
method: 'POST',
url: `${baseUrl}/v1/api/login`,
body: loginData,
}).then((response) => {
expect(response.status).to.eq(200);
});
});
});
});
To run the tests, return to the browser version managed by Cypress and select the specific test file you wish to run.
The Cypress test runner will run the tests and record their results, showing the pass or fail status of each test case.
The examples above illustrate how you can test various routes and their corresponding controller functions to ensure their functionality and expected behavior. While it is essential to test the functionality of APIs, you should not limit the testing scope solely to this aspect.
A comprehensive API testing strategy should also include tests on performance, load, and integration with other services. By including different types of testing methods in your strategy, you can achieve thorough test coverage and ensure that your APIs are both functional and reliable before deploying your code to production.
Testing Your Entire Web Experience Using Cypress
Cypress is a fantastic tool for testing web applications, seamlessly covering tests for both the front-end and back-end.
With its user-friendly testing features, you can easily and quickly set up a testing environment all in one platform. You can then use it to thoroughly test different aspects of your application and guarantee top-notch performance.