Які інструменти для тестування 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 скрипта який тестує точку входу в 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 команд для запезпечення зручності програмування і читабельності програмного коду.
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 у 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
Увесь код що надано вище є робочим і використовується у реальних системах.
Ваш тест може бути «зеленим», навіть якщо буде помилка, бо не обробляються всі стейти промісу з 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
Якщо помилка виникне в сайпрес скрипті то її перехоплювати немає необхідності бо просто потрібно буде пофіксити скрипт, як наприклад у випадку помилки у написанні будь-якого ідентифікатора.