Существует множество способов измерения производительности. В сегодняшнем посте я хочу рассказать об одном из самых простых. Представьте себе следующий сценарий:
- пользователь нажимает на кнопку
- появляется модальное окно
Наш тест может выглядеть примерно так:
cy.visit('/board/1')
// wait for loading to finish
cy.getDataCy('loading')
.should('not.exist')
cy.getDataCy('card')
.click()
Это модальное окно может получить некоторые данные с сервера, переупорядочить или отфильтровать их. Кроме того, оно может выполнять некоторые другие действия, такие как рендеринг изображений и т.д. Все эти действия занимают некоторое время, и мы, как тестеры, хотим быть уверены, что результат не займет слишком много времени.
API performance.mark()
Во всех современных браузерах API performance
доступен для объекта window
. Мы можем получить доступ к этому API с помощью функции cy.window()
и последующего вызова метода. Чтобы начать измерение производительности, мы можем создать метку, которая будет обозначать начало нашего измерения.
cy.visit('/board/1')
// wait for loading to finish
cy.getDataCy('loading')
.should('not.exist')
cy.window()
.its('performance')
.invoke('mark', 'modalOpen')
cy.getDataCy('card')
.click()
Цепочка, начинающаяся с cy.window()
, фактически делает то же самое, что если бы мы набрали window.performance.mark('modalOpen')
в нашей консоли DevTools. modalOpen
— это просто метка, и ее можно назвать как угодно.
API performance.measure()
Теперь, когда мы обозначили начало нашей метрики, давайте выполним следующие шаги. Когда мы нажимаем на карточку, открывается модальное окно. Во-первых, мы хотим убедиться, что достигли желаемого результата. Мы можем проверить это, сделав утверждение о видимости модального окна:
cy.visit('/board/1')
// wait for loading to finish
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()
для проведения измерения. По сути, здесь мы нажимаем кнопку на секундомере. Аргументом функции measure
будет наша метка modalOpen
. Причина передачи этого аргумента заключается в том, что мы можем добавить несколько меток в наш тест, и нам нужно указать, какую из них измерять. Для вызова функции measure мы, в основном, выполняем тот же набор функций Cypress, что и раньше:
cy.visit('/board/1')
// wait for loading to finish
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()
. Поскольку нам не нужна повторяемость, мы можем установить таймаут на 0 и сразу же сделать наше утверждение. Давайте сделаем утверждение, что модальное окно не должно загружаться дольше 2 секунд (2000 в миллисекундах).
cy.visit('/board/1')
// wait for loading to finish
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
. Эта библиотека является частью большего целого — namespace Cypress
.
Строки 11 — 29 — это функция, которая содержит нашу цепочку команд из предыдущего примера. Кроме того, я скрыл журналы трех наших команд и добавил свой собственный журнал, который вы можете увидеть в строках 15 — 24.
Наконец, в строке 31 мы добавляем эту функцию в библиотеку Cypress. В то время как строки 1-9 добавляют нашу команду в пространство имен Cypress, которое может распознать наш компилятор TypeScript, функция Cypress.Commands.addAll()
добавит ее в сам Cypress. Я обычно сохраняю свои пользовательские команды в папке cypress/support/commands/
и делаю import ../commands/mark.ts
внутри файла cypress/support/index.ts
.
declare namespace Cypress {
interface Chainable<Subject = any> {
/**
* Add a measurment marker. Used with cy.measure() command
* @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()
:
declare namespace Cypress {
interface Chainable<Subject = any> {
/**
* Add a measurment marker. Used with cy.measure() command
* @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()
заключается в том, что на этот раз тип возврата будет number
, потому что наша функция будет возвращать число. Также вместо использования функции .its()
мы возвращаем ее из функции .then()
, так как хотим использовать ее и в детализации нашей консольной команды. Если это много новых терминов, я предлагаю ознакомиться с этим постом об улучшении пользовательской команды Cypress, который я сделал ранее.
Тестирование производительности в Cypress
Всякий раз, когда мы проводим тестирование производительности любого типа, нам необходимо обратить пристальное внимание на среду, в которой мы тестируем. Находимся ли мы в производстве? Находится ли оно в настоящее время под большой нагрузкой? Если на промежуточном сервере, то 1:1 ли это с производством или мы тестируем уменьшенную версию? Используем ли мы браузер для тестирования производительности? Какой именно? Какую версию? Все эти и другие вопросы должны быть заданы, чтобы обеспечить контекст для показателей производительности.
В нашем контексте мы работаем в браузере, в котором открыты два iframe. Один для нашего приложения и один для скрипта Cypress. Это может повлиять на наше тестирование, и это не маловажно. Cypress предупреждает об этом в своей документации. Это не означает, что измерение производительности в Cypress бесполезно. Это просто означает, что мы должны принимать во внимание контекст, когда смотрим на метрики.