Get in touch

Testing Backend APIs with Cypress

What api testing tools do you know and use during development? The majority will probably call it Postman. Yes, this is a fairly common and well-known tool. Someone will say that it is jMeter, Fiddler, SoapUI, Advanced REST Client.

I want to offer you Cypress. Yes, the same Cypress that web application developers use for End2end testing and less often for testing React/Vue/Angular/Svetle components. It will be especially convenient for those who create APIs on Nodejs, because Cypress uses JavaScript. Cypress is a very flexible tool that can help to automate any actions of checking both client and server code. Cypress simulates working environment for api calls as much as possible because it runs scripts for testing directly from the browser.

How to install and start working with Cypress you can easily find by this link. Now I propose to dwell on moments that are not well described on the Internet. Namely, on testing server interfaces that require authorization.

For an example of an api that needs to be implemented and tested accordingly during development, let’s take a server implementation based on Firebase Callable function and protected by Firebase Authentication (which actually is Google Identity Toolkit wrapped in Firebase). We will stop at the implementation of the api directly another time. Now we are interested in how to check it for compliance with the requirements.

Using the implementation below, it is possible to test server interfaces on behalf of any user registered in the web application.

Cypress script

The code is a Cypress script that tests the api endpoint getList

describe('Users API Requests', () => { context.only('getList', () => { it('Success / for Admin', () => { cy.userApi("getList") .then((response) => { cy.log(response.body) expect(response.status).to.eq(200) }) }) })

cypress/e2e/backend/users-api.cy.js

Cypress commands

The auxiliary code is designed in the form of two Cypress commands to ensure ease of programming and readability of the program code.

  • userApi - main command that calls getIdTokenForAdmin to get IdentityToken and produce HttpsCallable request to server, using action parameter that in our case is getList

  • getIdTokenForAdmin - auxiliary command to obtain IdentityToken for user userEmailAdmin. An interesting example of cache implementation. If the token was formed earlier, the repeated formation is bypassed.

Cypress.Commands.add('userApi', (action, params = {}) => { cy.getIdTokenForAdmin().then(userIdTokenAdmin => { return cy.request({ url: `${beUrl}/users`, ...callable({ action, ...params }, userIdTokenAdmin) }) }) }) let userIdTokenAdmin; Cypress.Commands.add('getIdTokenForAdmin', () => { if (userIdTokenAdmin) return userIdTokenAdmin return cy.task("firebase:getUserIdToken", userEmailAdmin).then(token => { userIdTokenAdmin = token return token }) }) const callable = (parameters = {}, identityToken) => { if (!identityToken) throw Error('Parameter "identityToken" is mandatory for the function callable()') return { method: "POST", body: { data: { ...parameters }, }, failOnStatusCode: false, auth: { bearer: identityToken } } };

cypress/support/commands.js

Cypress Firebase plugin

Code that runs when Cypress starts in Nodejs (not in the browser). This is important to understand because it allows you to run firebase-admin and access the Firebase project management on the server. It is not possible to do this from the browser (where Cypress scripts are executed).

function initFirebasePlugin(on, config) { // process.env.GOOGLE_APPLICATION_CREDENTIALS = '/Users/omnigon/projects/omnigon-site/omnigon-site-gatsby/serviceAccount.json' // or the below const serviceAccount = require('../../.secure/serviceAccount.json'); initializeApp({ credential: cert(serviceAccount) }); registerFirebaseTasks(on, config) } function getUserIdToken(userEmail = "xxx@gmail.com") { return getAuth().getUserByEmail(userEmail) .then(userRecord => { return getAuth().createCustomToken(userRecord.uid) }) .then(customToken => { return axios.post( `https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken?key=${firebaseConfigStagingApiKey}`, { token: customToken, returnSecureToken: true, }, ); }) .then(idTokenResponse => idTokenResponse.data.idToken) } function registerFirebaseTasks(on, config) { on('task', { 'firebase:getUserIdToken'(email) { return getUserIdToken(email) } }) } module.exports.initFirebasePlugin = initFirebasePlugin

cypress/support/firebase-plugin.js

const { initFirebasePlugin } = require('./cypress/support/firebase-plugin'); module.exports = defineConfig({ ... setupNodeEvents(on, config) { initFirebasePlugin(on, config) }, });

cypress.config.js

Recommendations

  1. First and foremost, start writing tests once you write your first api code.
  2. Be sure to use the Firebase Emulator for development, or in your case anything that allows you to run and test the api locally. This will significantly speed up and simplify the development process for you.
  3. Create at least one test for each api entry point (Happy path per api endpoint)
  4. Write tests for all the most critical options that the system can get into. For example, checking that the api really gives an error when an unauthorized user tries to use it. (Unhappy paths)

All the code provided above is working and used in real systems.

Discuss on Linkedin

Ukrainian version

Date: August 09, 2023
Copyright 2009-2023 LLC "Omnigon." © All rights reserved.