Всем привет! На связи Наталья Данилина и Чечиков Иван из Звука. В этой статье мы хотим поделиться опытом внедрения 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, сократить время на выполнение рутинных задач, повысить качество тестирования и ускорить обнаружение ошибок в интерфейсе приложения.

Спасибо, что прочитали! Если у вас есть вопросы – будем рады ответить на них в комментариях.

Комментарии (2)


  1. stas_grishaev
    19.04.2024 07:52
    +1

    Для Allure TestOps интереснее дифф снепшотов записывать с использованием плагина screen-diff-plugin который генерит один файл формата vnd.allure.image.diff и позволяет просматривать дифф в удобном режиме


    1. Coder69
      19.04.2024 07:52
      +1

      Возьмем на заметку. Спасибо!