QA Interview Hub · База знань · Тести

Cypress

API-тестування

cy.request від простого до Senior

Що таке cy.request і чим він відрізняється від cy.intercept?

cy.request сам відправляє HTTP-запит — з Node-процесу Cypress, а не зі сторінки. Тому CORS його не блокує, а cookies автоматично надсилаються і зберігаються. Це інструмент для API-тестів і підготовки даних.

cy.intercept нічого не відправляє — він перехоплює запити, які робить застосунок.

cy.requestcy.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. Але навантажувальне тестування — так само не його задача.