Всем привет! На связи Наталья Данилина и Чечиков Иван из Звука. В этой статье мы хотим поделиться опытом внедрения snapshot-тестов для web-приложения — расскажем, что это такое и для каких задач применяется.
Подробности – под катом.
Все началось с поиска ответов на вопросы: как сократить время на тестирование, как не выгореть на регрессе, как потестить фичу без дизайн-ревью, и как сократить кол-во багов в проде после релиза?
В чем суть. Мы имеем большое количество ручных тест-кейсов, которые покрыты e2e и component-автотестами. Наши автоматизированные тесты запускаются каждый раз при старте регресса (при этом длительность его занимает от 3 до 4 дней). И бывают ситуации, что уже после завершения процесса мы находим баги в проде, связанные с UI — их сложно уловить на этапе регресса из-за человеческого фактора.
Поэтому мы нашли подход, который сможет в перспективе решить подобные проблемы — так в работе появились snapshot-тесты.
Snapshot-тесты — это автоматизированные тесты, которые используются для проверки веб-страниц. Они позволяют сравнить текущее состояние веб-страницы с предыдущим (или ожидаемым) состоянием и обнаружить любые изменения или различия. Мы используем snapshot-тесты также и для сравнения отдельных html-элементов (попапов, модалок, фреймов и пр.). Происходит сравнение текущего состояния компонента в тестовой среде и эталона. Эталоном может быть компонент прода, но лучше, чтобы это был компонент актуального дизайна.
Snapshot-тесты используют попиксельное сравнение. Каждый пиксель эталона сравнивается с соответствующим пикселем изображения объекта тестовой среды, чтобы обнаружить различия. Это позволяет тестам быть очень точными и обнаруживать даже небольшие изменения на веб-странице или в компоненте. Однако надо учитывать погрешность в сравнении, так как могут быть холостые ошибки при небольшом проценте отличия (до 5%).
Работая со snapshot-тестами, мы выделили ряд их преимуществ:
Быстрая обратная связь. Snapshot Testing позволяет быстро проверить, изменился ли результат теста после внесения изменений в код. Это дает быструю обратную связь разработчикам и тестировщикам, что помогает быстрее реагировать на возникающие проблемы.
Автоматизация тестирования интерфейса. Snapshot Testing автоматизирует процесс проверки интерфейса, что позволяет решать связанные с ним задачи быстрее и эффективнее.
Упрощение процесса тестирования. Snapshot Testing упрощает процесс тестирования интерфейса, так как не требует большого количества кода для написания тестов. Это сокращает время, затрачиваемое на написание и поддержку тестов, освобождая ресурсы для других задач.
Повышение качества кода. Благодаря snapshot-тестам можно легко отслеживать изменения в интерфейсе и быстро обнаруживать ошибки. Поэтому повышается качество кода и улучшается пользовательский опыт. В дальнейшем мы планируем использовать snapshot-тестирование в процессе разработки.
Сокращение времени на регрессионное тестирование. Внедрение Snapshot Testing позволяет сократить время, затрачиваемое на регрессионное тестирование. Поэтому проверка интерфейса автоматизируется, а проблемы выявляются быстрее.
Мы внедрили snapshot-тесты в наш UI-фреймворк, написанный на Kotlin с использованием Playwright. Под них мы создали отдельную директорию во фреймворке и выделили джобу для запуска тестов в GitLab CI. TMS у нас — Allure TestOps.
Сами тесты логически не сложны. Рассмотрим на примере теста сравнения куки в темной теме:
.....
class CompareCookiesBlackSnapshotsTest : WebTest() {
@Test
@TestOpsId(44908)
@DisplayName("Сравнение попапов кук (темная тема)")
@Owner(CHECHIKOV_IVAN)
fun testCase() {
step(
"""
|*Precondition*:
|Пользователь не авторизирован и находится на главной странице"
""".trimMargin()
) {
assertThat(page).containsURL(baseUrl)
page.waitForLoadState()
}
step("Проверяем, что включена темная тема") {
SideMenuBar(page).checkedSwitcherDarkTheme()
}
step("Проверяем что попап куки есть на странице") {
assertTrue(CookiesPage(page).cookiesBody.isVisible)
}
step("Сравниваем с эталоном") {
screenStageBytes = CookiesPage(page).cookiesBody.screenshot()
assertScreenShots(screenStageBytes, imageName = "cookieBlack")
}
}
}
.....
Шаги достаточно понятны: мы выполняем предусловие, проверяя URL главной страницы. Следующим шагом убеждаемся, что включена темная тема, далее — что присутствует кука на странице. В заключении мы делаем скриншот текущего состояния куки и вызываем метод сравнения тестового изображения с эталоном.
.....
fun assertScreenShots(stageBytesArray: ByteArray, imageName: String) {
val prodImage = ImageIO.read(File("src/test/resources/screens/expectedImages/$imageName.png"))
val stageImage = ImageIO.read(ByteArrayInputStream(screenStageBytes))
.....
Эталоны лежат у нас в директории /screens/expectedImages под гитом. В тестах мы заранее знаем, каким будет тот или иной эталон, поэтому пробрасываем в переменную $imageName название нашего изображения. Сделав скриншот тестового компонента, мы переводим его в объект BufferedImage, как и эталон.
.....
val prodImageResized = resizeImage(prodImage, stageImage.width, stageImage.height)
.....
.....
fun resizeImage(image: BufferedImage, width: Int, height: Int): BufferedImage {
val scaledImage = image.getScaledInstance(width, height, Image.SCALE_SMOOTH)
val resizedImage = BufferedImage(width, height, image.type)
resizedImage.getGraphics().drawImage(scaledImage, 0, 0, null)
return resizedImage
}
.....
Приводим объекты сравнения к одному размеру:
.....
val imageComparison = ImageComparison(prodImageResized, stageImage).setAllowingPercentOfDifferentPixels(5.0)
val diff = imageComparison.compareImages()
.....
Магию сравнения выполняет библиотека image comparision. В объект ImageComparison мы передаем наши BufferdImage объекты для сравнения и выставляем процент погрешности - 5.0. Проводим сравнение и получаем объект расхождения diff.
.....
val diffImageStream = ByteArrayOutputStream()
val prodImageStream = ByteArrayOutputStream()
val diffImg = diff.result
ImageIO.write(diffImg, "png", diffImageStream)
val diffBytesArray = diffImageStream.toByteArray()
ImageIO.write(prodImageResized, "png", prodImageStream)
val prodBytesArray = prodImageStream.toByteArray()
saveScreenshot("actual", stageBytesArray)
saveScreenshot("expected", prodBytesArray)
saveScreenshot("difference", diffBytesArray)
assertEquals(ImageComparisonState.MATCH, diff.imageComparisonState,
"Изображение в тестовой среде отличается от эталона $imageName")
}
.....
Мы хотим видеть в результатах прогона тестов изображения сравниваемого тестового компонента, эталона и изображения-различия, поэтому переводим все наши объекты сравнения в массив байтов и сохраняем их в скриншоты.
Как это выглядит в Allure TestOps:
Текст, иконки крестика и куки в эталоне отличаются от скриншота с тестовой среды. На изображении различия мы видим изменения, отмеченные красными прямоугольниками. Ниже сообщение об отличии изображений.
Итак, мы привели пример одного из наших snapshot-тестов. Cейчас мы продолжаем расширять покрытие, так как в долгосрочной перспективе надеемся, что Snapshot Testing поможет значительно улучшить эффективность работы команды QA, сократить время на выполнение рутинных задач, повысить качество тестирования и ускорить обнаружение ошибок в интерфейсе приложения.
Спасибо, что прочитали! Если у вас есть вопросы – будем рады ответить на них в комментариях.