Get in touch

Тестування серверного API за допомогою Cypress

Які інструменти для тестування api ви знаєте і якими користуєтесь під час розробки? Більшість мабуть назве Postman. Так, це досить поширений і загально відомий інструмент. Хтось скаже що це jMeter, Fiddler, SoapUI, Advanced REST Client.

Я хочу запропонувати вам Cypress. Так, той самий Cypress який розробники веб застосунків використовують для End2end тестування і рідше для тестування React/Vue/Angular/Svetle компонентів. Особливо зручно буде тим хто створює API на Nodejs, адже Cypress використовує JavaScript. Cypress це дуже гнучкий інструмент за допомою якого можна автоматизувати будь-які дії з перевірки як клієнтського так і серверного коду. Cypress максимально імітує робоче середовище для викликів api бо запускає скрипти для тестування беспосередньо з браузера, який, до речі, ви можете вибрати.

Як встановити і розпочати працювати з Cypress ви легко знайдете за цим посиланням. Зараз я пропоную зупинитись на моментах які мало описані в інтернеті. А саме на тестуванні серверних інтерфейсів які потребують авторизації.

Для прикладу api яке потрібно реалізувати і відповідно тестувати під час розробки візьмемо серверну реалізацію базовану на Firebase Callable function і захищену за допомогою Firebase Authentication (що насправді є Google Identity Toolkit загорнутим у Firebase). На реалізації безпосередньо api ми зупинимось іншим разом. Зараз нас цікавить як його перевіряти на відповідність вимогам.

Використовуючи наведену нижче реалізацію є можливість тестувати серверні інтерфейси від імені будь-якого зареєстрованого у веб застосунку користувача.

Cypress скрипт

Код безпосередньо Cypress скрипта який тестує точку входу в api 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 команди

Допоміжний код оформлений у вигляді двох Cypress команд для запезпечення зручності програмування і читабельності програмного коду.

  • userApi - основна команда, яка викликає getIdTokenForAdmin для отримання IdentityToken і формування HttpsCallable запиту на сервер, використовуючи параметр action що у нашому випадку є getList

  • getIdTokenForAdmin - допоміжна команда для отримання IdentityToken для користувача userEmailAdmin. Цікавий приклад імплементації кешу. Якщо токен формувався раніше то повторне формування оминається.

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

Ця частина коду виконується під час запуску Cypress у Nodejs (не в браузері). Це важливо для розуміння бо дає можливість запускати firebase-admin і отримати доступ до управління Firebase проектом на сервері. З браузера (де виконуються Cypress скрипти) це зробити не можливо.

const serviceAccount = require('../../.secure/serviceAccount.json'); function initFirebasePlugin(on, config) { 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

Рекомендації

  1. Перше і найголовніше - починайте писати тести одноразово з написанням першого коду api.
  2. Для розробки обов’яково використовуйте Firebase Emulator, або у вашому випадку будь-що що дає змогу запустити і тестувати api локально. Це значно прискорить і спростить для вас процес розробки.
  3. Створіть як мінімум один тест для кожної точки входу в api (Happy path per api endpoint)
  4. Пишіть тести для усіх найбільш критичних варіантів у які може потрапити система. Наприклад, перевірка того що api дійсно видає помилку коли ним намагається скористатися неавторизований на цю дію користувач. (Unhappy pathes)

Увесь код що надано вище є робочим і використовується у реальних системах.

Question from DOU

Запитання:

Ваш тест може бути «зеленим», навіть якщо буде помилка, бо не обробляються всі стейти промісу з cy.userApi(). Або додайте .catch і фейліть тест, або переробіть на async/await.

Відповідь:

Твердження про використання catch є поширеною помилкою розробників на початку знайомства з Cypress. В Cypress саме у такій комбінації відсутня можливість використання catch. Причина у тому, що Cypress command повертає не проміс а власну обгортку яка обробляє лише then.

На данну тему є гарна секція у документації Introduction to Cypress - You cannot add a .catch error handler to a failed command:

Або це обговорення на Stackoverflow Cypress has no .catch command

Зрештою, якщо помилка виникне

Якщо помилка виникне на сервері, то він поверне статус > 400. При цьому параметр failOnStatusCode команди cy.request постійно встановлюється у false
failOnStatusCode - Whether to fail on response codes other than 2xx and 3xx

Якщо помилка виникне в сайпрес скрипті то її перехоплювати немає необхідності бо просто потрібно буде пофіксити скрипт, як наприклад у випадку помилки у написанні будь-якого ідентифікатора.

Додаткові посилання

Обговорення на dou.ua. Дякую за коментарі/запитання

English version

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