QA Interview Hub · База знань · Тести
Cypress
API-тестування
cy.request від простого до Senior
Що таке cy.request і чим він відрізняється від cy.intercept?
cy.request сам відправляє HTTP-запит — з Node-процесу Cypress, а не зі сторінки. Тому CORS його не блокує, а cookies автоматично надсилаються і зберігаються. Це інструмент для API-тестів і підготовки даних.
cy.intercept нічого не відправляє — він перехоплює запити, які робить застосунок.
| cy.request | cy.intercept |
| Що робить | Відправляє запит | Перехоплює запит сторінки |
| Звідки | Node-процес | Проксі Node-процесу |
| Навіщо | API-тести, логін, підготовка даних | Моки, перевірка запитів фронтенду |
На співбесіді: cy.request сам відправляє HTTP-запит, і робить це з Node-процесу, тому CORS йому не заважає. Я використовую його для API-тестів, логіну і підготовки даних. cy.intercept нічого не відправляє — він перехоплює запити, які робить застосунок, щоб їх підмінити або перевірити.
А в Playwright:Аналог cy.request — фікстура request (APIRequestContext). Головна відмінність: браузер не запускається, тож API-тести набагато швидші й паралеляться воркерами. page.request ділить cookies зі сторінкою.
Як перевірити статус, тіло й заголовки відповіді?
cy.request віддає об'єкт відповіді з status, body, headers і duration. Окремі поля зручно дістати через .its(), кілька перевірок — у .then().
cy.request('GET', '/api/products/42').then((res) => {
expect(res.status).to.eq(200);
expect(res.headers['content-type']).to.include('application/json');
expect(res.body).to.have.property('id', 42);
expect(res.body.price).to.be.a('number').and.be.greaterThan(0);
expect(res.duration).to.be.lessThan(1000);
});
cy.request('/api/products').its('body').should('have.length.greaterThan', 0);
На співбесіді: cy.request повертає об'єкт відповіді зі статусом, тілом, заголовками і тривалістю. Одне поле я дістаю через its, а кілька перевірок роблю в then: статус, content-type, наявність і тип полів у тілі, і за потреби — що відповідь прийшла швидше за певний поріг.
А в Playwright:const res = await request.get('/api/products/42');
await expect(res).toBeOK();
expect(res.headers()['content-type']).toContain('application/json');
const body = await res.json();
expect(body).toMatchObject({ id: 42 });
Як тестувати негативні сценарії: 400, 401, 404?
За замовчуванням cy.request падає на будь-якому статусі 4xx чи 5xx. Для негативних тестів потрібна опція failOnStatusCode: false, інакше тест упаде ще до твоєї перевірки. Це одне з найчастіших питань.
cy.request({
method: 'POST',
url: '/api/orders',
body: { items: [] },
failOnStatusCode: false,
}).then((res) => {
expect(res.status).to.eq(400);
expect(res.body.message).to.contain('items');
});
cy.request({ url: '/api/admin', failOnStatusCode: false })
.its('status').should('eq', 401);
На співбесіді: За замовчуванням cy.request падає на будь-якому статусі чотириста чи п'ятсот. Тому для негативних кейсів я додаю failOnStatusCode false і вже сам перевіряю, що прийшов очікуваний код — чотириста при невалідних даних, чотириста один без токена, чотириста чотири для неіснуючого ресурсу — і що повідомлення про помилку осмислене.
А в Playwright:Протилежна поведінка: Playwright не падає на 4xx і 5xx за замовчуванням, статус перевіряєш сам. Щоб падало, як у Cypress, є опція failOnStatusCode: true.
Як побудувати CRUD-ланцюжок в одному тесті?
Створюєш сутність, береш id з відповіді, читаєш, оновлюєш і видаляєш, а наприкінці перевіряєш, що вона справді зникла. Кожен крок передає дані наступному через .then().
it('CRUD товару', () => {
cy.request('POST', '/api/products', { name: 'Термос', price: 500 })
.then(({ status, body }) => {
expect(status).to.eq(201);
const id = body.id;
cy.request('GET', '/api/products/' + id)
.its('body.name').should('eq', 'Термос');
cy.request('PUT', '/api/products/' + id, { name: 'Термос', price: 450 })
.its('body.price').should('eq', 450);
cy.request('DELETE', '/api/products/' + id)
.its('status').should('eq', 204);
cy.request({ url: '/api/products/' + id, failOnStatusCode: false })
.its('status').should('eq', 404);
});
});
На співбесіді: Я створюю сутність через POST, беру id з відповіді і передаю його далі через then. Потім читаю через GET і перевіряю дані, оновлюю через PUT, видаляю через DELETE, а наприкінці переконуюся, що GET повертає чотириста чотири. Так один тест покриває весь життєвий цикл ресурсу і сам за собою прибирає.
А в Playwright:Логіка та сама, але через await без вкладених колбеків — ланцюжок читається зверху вниз, а id лягає у звичайну змінну.
Як організувати авторизацію в API-тестах?
Логін робиш запитом, токен зберігаєш і передаєш у заголовку Authorization. Щоб не повторювати код, винось у кастомну команду. Логін і пароль — зі змінних оточення, а не з коду.
// cypress/support/commands.ts
Cypress.Commands.add('apiLogin', () => {
return cy.request('POST', '/api/auth/login', {
email: Cypress.env('USER_EMAIL'),
password: Cypress.env('USER_PASSWORD'),
}).its('body.token');
});
// тест
cy.apiLogin().then((token) => {
cy.request({
url: '/api/orders',
headers: { Authorization: 'Bearer ' + token },
}).its('status').should('eq', 200);
});
На співбесіді: Я логінюся запитом, дістаю токен і передаю його в заголовку Authorization. Логіку винесено в кастомну команду apiLogin, щоб не дублювати. Облікові дані беру зі змінних оточення, вони не потрапляють у репозиторій.
А в Playwright:Зручно створити окремий контекст із заголовком і віддавати його через фікстуру.
const api = await playwright.request.newContext({
baseURL: process.env.API_URL,
extraHTTPHeaders: { Authorization: 'Bearer ' + token },
});
Як валідувати схему відповіді?
Перевірка окремих полів не ловить ситуацію, коли бекенд змінив тип чи прибрав поле, яке ти не перевіряв. Валідація JSON-схеми перевіряє всю структуру: обов'язкові поля, типи, формати.
Найпростіше — підключити плагін chai-json-schema і зберігати схеми у фікстурах. Альтернатива — ajv через cy.task або власну команду.
// cypress/support/e2e.ts
import chaiJsonSchema from 'chai-json-schema';
chai.use(chaiJsonSchema);
// тест
cy.fixture('schemas/product.json').then((schema) => {
cy.request('/api/products/42').its('body')
.should('be.jsonSchema', schema);
});
// fixtures/schemas/product.json
// { "type": "object",
// "required": ["id", "name", "price"],
// "properties": {
// "id": { "type": "integer" },
// "name": { "type": "string" },
// "price": { "type": "number", "minimum": 0 } } }
На співбесіді: Перевірка окремих полів пропускає зміни, яких ти не передбачив, наприклад коли бекенд змінив тип поля. Тому я валідую всю відповідь за JSON-схемою: підключаю chai-json-schema, а самі схеми зберігаю у фікстурах. Так тест одразу падає, якщо змінився контракт.
А в Playwright:Chai-плагіни не потрібні: тест у Node, тож ajv підключається напряму, а результат перевіряється через expect(validate(body)).toBe(true).
Як поєднувати API і UI в одному тесті?
Підготовка даних через API, перевірка через UI, прибирання знову через API. Це в рази швидше й стабільніше, ніж створювати все кліками, і тестує саме те, що треба.
Другий прийом — перевірити, що фронтенд відправив правильний запит: дія в UI, intercept з alias, перевірка тіла запиту.
let productId;
beforeEach(() => {
cy.request('POST', '/api/products', { name: 'Тестовий товар', price: 99 })
.its('body.id').then((id) => { productId = id; });
});
afterEach(() => {
cy.request('DELETE', '/api/products/' + productId);
});
it('товар видно в каталозі', () => {
cy.visit('/catalog');
cy.contains('[data-cy=product]', 'Тестовий товар').should('be.visible');
});
На співбесіді: Я створюю потрібні дані через API в beforeEach, сам тест перевіряє тільки UI, а в afterEach прибираю дані знову через API. Це набагато швидше і стабільніше, ніж створювати все кліками. Той самий підхід працює і в Playwright: тестові об'єкти можна видаляти через API після кожного тесту.
А в Playwright:Те саме, але через фікстуру: створення до await use(), видалення після. Прибирання гарантоване навіть при падінні тесту, і цю фікстуру просто додаєш у сигнатуру потрібного тесту.
Які обмеження має Cypress для API-тестування і коли брати інший інструмент?
- Немає вбудованої паралелізації без Cypress Cloud або плагінів.
- WebSocket напряму не тестується.
- Не підходить для навантажувального тестування — для цього k6 або JMeter.
- Для API-тестів усе одно піднімається браузер, тому це повільніше за чисті HTTP-клієнти. Зручніший вивід дає плагін cypress-plugin-api.
Великий набір чистих API-тестів зручніше тримати в Postman/Newman, Playwright request або Supertest. Cypress виправданий, коли API-тести переважно обслуговують UI-тести і команда хоче один стек.
На співбесіді: Cypress підходить для API-тестів, але має обмеження: без Cloud немає паралелізації, WebSocket напряму не тестується, навантаження ним не перевірити, і навіть для API-тестів піднімається браузер. Якщо API-тестів багато і вони самостійні, я б узяв Postman з Newman або Playwright request. Cypress виправданий, коли API здебільшого допомагає UI-тестам і хочеться тримати один стек.
А в Playwright:Для API Playwright зручніший: без браузера, паралельно через воркери, з HTML-звітом. WebSocket можна слухати через page.on('websocket') або мокати через routeWebSocket. Але навантажувальне тестування — так само не його задача.