Разделы портала

Онлайн-тренинги

.
Тестирование производительности фронтэнда с Cypress
21.07.2022 00:00

Автор: Филип Рик (Filip Hric)
Оригинал статьи
Перевод: Ольга Алифанова

Существует множество способов измерения производительности. В сегодняшней статье я хочу поговорить об одном из самых простых. Вообразите следующий сценарий:


  1. Пользователь нажимает на кнопку.
  2. Появляется модальное окно.

Наш тест может выглядеть как-то так:

cy.visit('/board/1')
 
// подождать окончания загрузки
cy.getDataCy('loading')
     .should('not.exist')
 
cy.getDataCy('card')
     .click()

Это модальное окно может получать какие-то данные от сервера, перегруппировывать или фильтровать их. К тому же оно может выполнять какие-то другие функции – например, отрисовку изображений. Все эти действия занимают время и, как тестировщики, мы должны убедиться, что результат не займет много времени.

performance.mark() API

Во всех современных браузерах API производительности доступно для объекта окна. Мы можем получить доступ к этому API, используя функцию cy.window(), а затем вызывая метод. Для начала замера производительности можно создать отметку старта нашего замера.

cy.visit('/board/1')
 
// подождать окончания загрузки
cy.getDataCy('loading')
     .should('not.exist')
 
cy.window()
     .its('performance')
     .invoke('mark', 'modalOpen')
 
cy.getDataCy('card')
     .click()

Подсвеченная часть кода делает то же, что произошло бы при вводе window.performance.mark('modalOpen') в консоли DevTools. modalOpen – это просто метка, ее можно назвать как угодно.

performance.measure() API

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

cy.visit('/board/1')
 
// подождать окончания загрузки
cy.getDataCy('loading')
     .should('not.exist')
 
cy.window()
     .its('performance')
     .invoke('mark', 'modalOpen')
 
cy.getDataCy('card')
     .click()
 
cy.getDataCy('card-detail')
     .should('be.visible')

После этого можно вызвать функцию performance.measure() для проведения измерений. В целом мы нажимаем кнопку по таймеру. Аргумент замеряющей функции – это наша метка modalOpen. Мы передаем этот аргумент, так как можем добавить в тест множество меток, и надо уточнить, что именно замерять. Чтобы вызвать функцию, мы выполняем тот же набор функций Cypress, что и ранее:

cy.visit('/board/1')
 
// дождаться окончания загрузки
cy.getDataCy('loading')
     .should('not.exist')
 
cy.window()
     .its('performance')
     .invoke('mark', 'modalOpen')
 
cy.getDataCy('card')
     .click()
 
cy.getDataCy('card-detail')
     .should('be.visible')
 
cy.window()
     .its('performance')
      .invoke('measure', 'modalOpen')

Команда invoke вернет объект с различными результатами:

 

Внутри команды можно выбрать свойство из объекта, используя команду .its(). Так как нам не нужно пробовать повторно, мы можем установить нулевой таймаут и сразу провести проверку. Проверим, что модальное окно должно загружаться не более чем за 2 секунды (2000 миллисекунд).

cy.visit('/board/1')
 
// дождаться окончания загрузки
cy.getDataCy('loading')
     .should('not.exist')
 
cy.window()
     .its('performance')
     .invoke('mark', 'modalOpen')
 
cy.getDataCy('card')
     .click()
 
cy.getDataCy('card-detail')
     .should('be.visible')
 
cy.window()
     .its('performance')
     .invoke('measure', 'modalOpen')
     .its('duration', { timeout: 0 })
     .should('be.lessThan', 2000)

Создание кастомной команды

Теперь, когда мы знаем, что делать, мы можем создать соответствующую кастомную команду. Тут много TypeScript, поэтому давайте поясню, что происходит. Строки 1-9 – это объявление типа. Так мы говорим компилятору TypeScript, что мы добавили новую команду cy.mark() в библиотеку cy-команд. Эта библиотека называется Chainable и содержит все команды cy. Это часть более крупного пространства имен Cypress.

Строки 11-29 – это функция, содержащая нашу цепь команд из предыдущего примера. В дополнение я спрятал логи трех наших команд и добавил свой собственный (строки 15-24).

И, наконец, на строке 31 мы добавляем эту функцию в библиотеку cypress. Строки 1-9 добавляют нашу команду в пространство имен Cypress, которое может распознать компилятор TypeScript, а функция Cypress.Commands.addAll() добавит ее в сам Cypress. Обычно я храню мои кастомные команды в папке cypress/support/commands/ folder и выполняю import ../commands/mark.ts внутри файла cypress/support/index.ts.

support/commands/mark.ts
declare namespace Cypress {
     interface Chainable<Subject = any> {
           /**
           * Добавление маркера изменений. Используется с командой cy.measure()
           * @example cy.mark('modalWindow')
           */
           mark: typeof mark
      }
}
const mark = (markName: string): Cypress.Chainable<any> => {
     const logFalse = { log: false }
     Cypress.log({
           name: 'mark',
           message: markName,
           consoleProps() {
                 return {
                    command: 'mark',
                     'mark name': markName
              }
         }
     })
return cy.window(logFalse)
     .its('performance', logFalse)
      .invoke(logFalse, 'mark', markName)
}
Cypress.Commands.addAll({ mark })

Схожим образом мы можем добавить и команду cy.measure():

support/commands/measure.ts
declare namespace Cypress {
      interface Chainable<Subject = any> {
          /**
          * Добавление маркера изменений. Используется с командой cy.measure()
          * @example cy.measure('modalWindow')
           */
            measure: typeof measure
     }
}
const measure = (markName: string): Cypress.Chainable<number> => {
     const logFalse = { log: false }
     let measuredDuration: number
     let log = Cypress.log({
          name: 'measure',
          message: markName,
          autoEnd: false,
          consoleProps() {
                return {
                    command: 'measure',
                    'mark name': markName,
                     yielded: measuredDuration
               }
          }
     })
     return cy.window(logFalse)
          .its('performance', logFalse)
          .invoke(logFalse, 'measure', markName)
          .then( ({ duration }) => {
                measuredDuration = duration
                log.end()
                return duration
          })
 }
Cypress.Commands.addAll({ measure })

Небольшое отличие от cy.mark() тут в том, что в этом случае возвращается число, потому что так работает наша функция. Также вместо функции .its() мы возвращаем значение из функции .then(), потому что мы хотим использовать ее в консоли. Если тут много новых терминов, посмотрите статью об улучшении ранее созданных кастомных команд Cypress.

Тестирование производительности в Cypress

Проводя тестирование производительности любого рода, нужно обращать особое внимание на окружение, в котором мы тестируем. Мы на проде? Под большой ли он нагрузкой? Если мы на стейдже, это 1:1 с продом, или это версия меньшего масштаба? Используем ли мы браузер для тестирования производительности? Какой? Какую версию? На эти и многие другие вопросы нужно найти ответы, чтобы метрики производительности получили контекст.

В нашем контексте мы запускаем тест в браузере, в котором открыто два фрейма – один для приложения, и один для скрипта Cypress. Это может значительно повлиять на наше тестирование, документация Cypress предупреждает об этом. Это не значит, что тестировать производительность в Cypress бессмысленно, это значит только, что нам нужно учитывать контекст, оценивая метрики.

Обсудить в форуме