August 16, 2022

Перехват сетевых запросов с помощью Cypress

Cypress удовлетворяет потребности в тестировании современных веб-приложений. В этой статье познакомимся с некоторыми возможностями команды Cypress .intercept(). Это очень полезный инструмент, особенно для тестирования труднодоступных мест вашего приложения.

Сейчас мы будем проверять её на базе приложения-клона Trello. Это простое приложение Vue.js, созданное с использованием json-сервера - однофайловой базы данных json.

URL

При использовании .intercept() команды, необходимо решить два основных вопроса.

  • Как нам сопоставить URL-адрес?
  • Как мы обрабатываем этот URL?

Давайте сосредоточимся на первом вопросе.

Рассмотрим следующий тест Cypress:

it('creating a board', () => {
  cy.intercept('/api/boards').as('matchedUrl')
  cy.visit('/')
  cy.get('[data-cy="create-board"]').click()
  cy.get('[data-cy=new-board-input]').type('new board{enter}')
})

Наша .intercept() команда соответствует любому запросу, содержащему URL-адрес /api/boards. Вы можете увидеть это на скриншоте ниже.

С нашим .intercept()мы сопоставили три разных запроса. Чтобы настроить таргетинг, например, только на наш POST /api/boardsзапрос, мы напишем нашу команду следующим образом:

cy.intercept('POST', '/api/boards').as('matchedUrl')

Также можем сопоставлять наши запросы с помощью регулярного выражения или строки минимального соответствия, поэтому для нацеливания нашего GET /api/boards/87116996032запроса, который запускается сразу после того, как мы входим в представление нашей доски, мы можем сделать следующее:

cy.intercept('GET', '/api/boards/*').as('matchedUrl')

Это полезно, когда мы используем настоящий API во время тестирования нашего внешнего интерфейса и получаем сгенерированный идентификатор, поступающий с сервера. С Minimatch нам все равно, какой будет окончательный идентификационный номер, мы сопоставим все, что соответствует нашим требованиям.

Можно сопоставить URL-адрес, используя несколько сопоставителей. Чтобы раскрыть весь потенциал, вы можете передать объект сопоставления в качестве первого аргумента:

cy.intercept({
  https: false
  method: 'GET',
  query: {
    limit: 10
  },
  path: '/api/boards'
})

Полный список атрибутов можно найти в документации Cypress .

Давайте теперь посмотрим, каково практическое использование .intercept()команды и как ее можно использовать для тестирования труднодоступных случаев.

Проведем тест

В нашем следующем тесте мы столкнулись со странной ситуацией. Давайте сначала посмотрим на код

import * as db from '../fixtures/oneBoard.json'
beforeEach(() => {
  cy.task('setupDb', db) // seed database
})
it('shows board list', () => {
  cy.intercept({
    method: 'GET',
    path: '/api/boards',
  }).as('matchedUrl')
  cy.visit('/')
  cy.get('[data-cy=board-item]').should('have.length', 0)
})

После проведения тестирования, мы получаем не тот результат, который нам нужен. Причина завала прохождения теста заключается в том, что наша .get()команда на самом деле не ожидает ответа от сетевого запроса. Она выбирает наши элементы, как только они появляются в DOM. Возможно, они еще даже не были видны.

Из-за этого наш тест проходит, пока наше приложение все еще находится в состоянии «загрузки», ожидая, пока сервер вернет список наших досок.

Поскольку мы сопоставили наш GET /api/boardsзапрос с .intercept()командой, мы можем убедиться, что наш тест продолжится только после того, как мы получим правильный ответ на наш запрос. Используя matchedUrlпсевдоним, который мы присвоили нашему перехвату, мы можем использовать .wait()такую команду:

import * as db from '../fixtures/oneBoard.json'
beforeEach(() => {
  cy.task('setupDb', db) // seed database
})
cy.intercept({
  method: 'GET',
  path: '/api/boards',
}).as('matchedUrl')
cy.visit('/')
cy.wait('@matchedUrl')
cy.get('[data-cy=board-item]').should('have.length', 0)

Это гарантирует, что наш тест будет стабильным и действительно даст нам правильный результат. Наша .get()функция на самом деле будет ждать, пока наша .wait()команда разрешится, и только затем начнет искать элементы элементов нашей доски. Теперь, когда наш .wait()на месте, наш тест не пройдет должным образом, так как в списке присутствует плата.

Тестирование API приложения

Теперь, когда мы успешно сопоставили наши запросы, мы можем продвинуться еще дальше в нашем тесте и объединить наш тест пользовательского интерфейса с небольшим тестированием API. После того, как мы направим наш запрос и дождемся ответа, мы можем передать данные нашей .then()функции и сделать некоторые утверждения:

it('creating a board', () => {
  cy.intercept('POST', '/api/boards').as('createBoard')
  cy.visit('/')
  cy.get('[data-cy="create-board"]').click()
  cy.get('[data-cy=new-board-input]').type('new board{enter}')
  cy.wait('@createBoard').then(({response}) => {
    expect(response.statusCode).to.eq(201)
    expect(response.body.name).to.eq('new board')
  })
})

Используя .then()и expect(), мы проверили правильный код состояния и часть тела ответа. Теперь мы действительно знаем, что как только мы создадим доску, мы получим правильный ответ от нашего сервера.

Может быть, даже более полезным, чем проверка ответа, может быть проверка нашего запроса. Таким образом мы можем убедиться, что наше приложение отправляет правильные данные на наш сервер. Например, мы можем убедиться, что заголовки запроса правильно прикреплены к запросу:

cy.wait('@createBoard').then(({request}) => {
  expect(request.headers.authorization).to.eq(`Bearer ${Cypress.env('TOKEN')}`)
})

Мы можем сделать еще один шаг вперед с функцией обработчика маршрута. Вместо того, чтобы передавать объект в качестве обработчика маршрута, наша .intercept()команда примет функцию и динамически изменит ответ нашего сервера.

В нашем следующем примере у нас есть список досок. С помощью нашей функции обработчика маршрута мы меняем атрибут, starredчтобы он имел значение true. Итак, мы берем реальные данные с нашего сервера и динамически меняем значения, которые хотим изменить.

import * as db from '../fixtures/twoBoards.json'
beforeEach(() => {
  cy.task('setupDb', db) // seed datbase
})
it('shows starred boards', () => {
  cy.intercept(
    {
      method: 'GET',
      path: '/api/boards',
    },
    ({reply, headers}) => {
      delete headers['if-none-match'] // prevent caching
      reply(({body}) => {
        body.map((board) => (starred = true))
      })
    },
  ).as('matchedUrl')
  cy.visit('/')
})

Это особенно полезно, когда вы хотите использовать только часть ваших реальных данных. Не нужно создавать все с нуля, просто настройте пару атрибутов и посмотрите, как приложение ведет себя должным образом.

При использовании .intercept()команды нужно учитывать несколько подводных камней . В показанном примере вы можете видеть, что мы удаляем if-none-matchзаголовок, чтобы предотвратить получение кешированного ответа от сервера. Это подводит нас к другому практическому варианту использования, связанному с изменением заголовков.

Обработка авторизации

Наше приложение использует очень простую авторизацию с помощью токена. Когда у нас есть этот токен, мы можем использовать его и добавлять в качестве заголовка авторизации ко всем нашим запросам.

it('shows private boards', () => {
  cy.intercept('/api/*', ({headers}) => {
    headers['Authorization'] = `Bearer ${Cypress.env('TOKEN')}`
  }).as('matchedUrl')
  cy.visit('/')
})

Теперь, когда мы добавляем этот заголовок ко всем нашим запросам, наше приложение ведет себя так, как если бы мы вошли в систему.

Со всеми этими вариантами определенно весело пробовать, но полезно напомнить себе о том, какой должна быть цель теста.

.intercept()безусловно, хорош для тестирования труднодоступных крайних случаев. Но использование настоящего API для ваших тестов определенно имеет ряд преимуществ. Во-первых, это настоящий API. Каждый раз, когда мы заглушаем наш ответ, мы отклоняемся от того, что дал бы нам настоящий сервер. Это может привести к ложным срабатываниям и позволить ошибке проникнуть в рабочую среду.​