Тестирование скриншотами

    Для будущих студентов курса «Python QA Engineer» подготовили авторскую статью.

    Также приглашаем на открытый вебинар по теме
    «Непрерывная интеграция с Jenkins». Рассмотрим, как настраивать автоматический запуск тестов, устанавливать плагины и создавать бекапы конфигураций сборок.


    Здравствуйте! Сегодня хочу рассказать о нашем опыте тестирования скриншотами с использованием python, selenium, и Pillow.

    Зачем? У нас был довольно большой (~1000) набор тестов на стеке python, pytest, selenium, которые отлично проверяли, что кнопки кликаются, а статистика отправляется (с использованием browserup proxy), но пропускали баги типа таких:  

    Используя только селениум, практически невозможно обнаружить такую неполадку, пока она не случится (не будешь же для каждого элемента добавлять проверку для каждого из его стилей). 

    Также проблема в том, что у selenium’a трудности с определением видимости некоторых компонентов, а значит, и протестировать верстку с его помощью затруднительно. 

    Посмотрим, как selenium определяет видимость для элементов карусели:

    Скрипт:

    from selenium.webdriver import Chrome
    from collections import Counter
    
    driver = Chrome()
    driver.get("https://go.mail.ru/search?q=%D1%86%D0%B2%D0%B5%D1%82%D0%BE%D1%87%D0%BA%D0%B8%20%D0%BA%D0%B0%D1%80%D1%82%D0%B8%D0%BD%D0%BA%D0%B8")
    elements = driver.find_elements_by_css_selector(".SmackPicturesContent-smackImageItem")
    print(Counter([el.is_displayed() for el in elements]))
    driver.quit()

    Напечатает Counter({True: 10}), хотя видимых элементов явно не 10, и независимо от того, какой сегмент карусели отображается, количество «видимых» карточек не меняется, из-за чего невозможно проверить скролл. 

    Аналогично будут работать и явные ожидания (visibility_of, visibility_of_all_elements_located, etc), так как они просто дергают у элемента is_displayed

    Решение

    Нам был нужен инструмент для pytest и selenium, чтобы мы могли переиспользовать инфраструктуру, на которой крутились текущие тесты. После поиска выяснилось, что ничего готового нет, а то что есть — давно не поддерживается. Поэтому решили сделать сами. 

    В общем это работает так:

    • открываем страницу селениумом, готовим ее к тесту (заполняем поля, жмем кнопки);

    • скриншотим всю страницу, и вырезаем кусок, на котором размещен компонент, который нужно протестировать;

    • вырезаем его, повторяем все на стейджинге и попиксельно сравниваем получившиеся картинки;

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

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

    Рабочий пример на гитхабе.

    В итоге тест выглядит так:

    def test_search_block(self):
       self.driver.get("https://go.mail.ru/")
    
       def action():
           self.driver.find_element_by_xpath("//span[contains(text(), 'Соцсети')]").click()
    
       self.check_by_screenshot((By.CSS_SELECTOR, ".MainPage-verticalLinksWrapper"), action=action)

    В action кладем все нужные сетапы, при необходимости финализацию можно добавить аналогичным способом.

    После завершения теста у нас получится три изображения с тестинга, с прода и дифф. Все три добавляем в отчет.

    Благодаря плагину для аллюра отчет выглядит так:

     И в случае падения:

    Проблемы

    1. Антиалайзинг — особенно сильно флакали тесты, где на скриншот попадали svg. Решили «смазав» границу между цветами:

    RED = "red"
    GREEN = "green"
    BLUE = "blue"
    ALPHA = "alpha"
    
    # https://github.com/rsmbl/Resemble.js/blob/dec5ae1cf1d10c9027a94400a81c17d025a9d3b6/resemble.js#L121
    # https://github.com/rsmbl/Resemble.js/blob/dec5ae1cf1d10c9027a94400a81c17d025a9d3b6/resemble.js#L981
    tolerance = {
       RED: 32,
       GREEN: 32,
       BLUE: 32,
       ALPHA: 32,
    }
    def _is_color_similar(self, a, b, color):
       """Проверить схожесть цветов. Для того, чтобы тесты не тригеррились на антиалиасинг допуски
    
       в self.tolerance.
       """
       if a is None and b is None:
           return True
    
       diff = abs(a - b)
    
       if diff == 0:
           return True
       elif diff < self.tolerance[color]:
           return True
    
       return False

    Так же сделано в Resemble.js. Надо сказать, что это диалектически влияет на надежность тестов. С одной стороны мы можем упустить проблемы с «одинаковыми» цветами, с другой тесты практически перестают моргать из-за сглаживания.

    Для особо тяжелых случаев просто удаляем проблемный элемент со страницы, например, так:

    def test_search_block(self):
       self.driver.get("https://go.mail.ru/")
    
       def action():
           element = self.driver.find_element_by_xpath("//span[contains(text(), 'Соцсети')]")
           self.driver.execute_script("arguments[0].remove()", element)
    
       self.check_by_screenshot((By.CSS_SELECTOR, ".MainVerticalsNav-listItemActive"), action=action)

    Примерно так же можно добавить элементу заливку, удалить изображение, и т.д.

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

    2. Не актуальные эталоны — проблема решилась сама собой благодаря отказу от эталонов.  Заодно нет проблемы с необходимостью следить за тем, чтобы эталоны были сделаны там же, где гоняются тесты (на разных платформах разные шрифты и не только).

    3. Для мобильных тестов (андроид эмуляторы и эмуляция в хроме) нужно учитывать плотность пикселей. Так получаем размеры и координаты элемента:

    def _get_raw_coords_by_locator(self, locator_type, query_string):
       """Без учета плотности пикселей."""
       wait = WebDriverWait(self.driver, timeout=10, ignored_exceptions=Exception)
       wait.until(lambda _: self.driver.find_element(locator_type, query_string).is_displayed(),
                       message="Невозможно получить размеры элемента, элемент не отображается")
      
       el = self.driver.find_element(locator_type, query_string)
       location = el.location
       size = el.size
       x = location["x"]
       y = location["y"]
       width = location["x"] + size['width']
       height = location["y"] + size['height']
       return x, y, width, height

    А так делаем поправку на плотность пикселей:

    def _get_coords_by_locator(self, locator_type, query_string) -> Tuple[int, int, int, int]:
       x, y, width, height = self._get_raw_coords_by_locator(locator_type, query_string)
       return x * self.pixel_ratio, y * self.pixel_ratio, width * self.pixel_ratio, height * self.pixel_ratio

    Если не учесть этого, скриншотится будет все что угодно, кроме нужных элементов.

    Размер элементов, которые возвращает селениум:

    from selenium.webdriver import Chrome, ChromeOptions
    
    options = ChromeOptions()
    options.add_experimental_option("mobileEmulation", {'deviceName': "Nexus 5"})
    options.add_argument('--headless')
    caps = options.to_capabilities()
    driver = Chrome(desired_capabilities=caps)
    driver.get("https://go.mail.ru/")
    print(driver.find_element_by_xpath("//body").size)
    driver.save_screenshot("test.png")
    driver.quit()

    Напечатает: {'height': 640, 'width': 360} для боди страницы.

    И в тоже время вернет скриншот размером 1080 х 1920:

    ❯ file test.png
    test.png: PNG image data, 1080 x 1920, 8-bit/color RGBA, non-interlaced

    4.Тесты никогда не будут зелеными во время релиза. Любые изменения в покрытых компонентах заставят тесты покраснеть, или сделают их сравнение невозможным (если расползутся размеры на тестинге и стейджинге). У этого есть и плюс: на диффе в отчете сразу видны изменения, в том числе те, которых не ожидали. 

    Итог

    Сейчас у нас ~570 скриншот-тестов в двух сборках (мобильная и десктопная). Каждая сборка запущена в 20 потоков, и идет около 15 минут. С учетом изменений в релизах, которых ломают тесты, флакающих — не больше 2-3%. В основном тесты падают из-за регресса, или изменений в верстке. Писать их тоже достаточно легко, при необходимости проверку скриншотами можно совместить с обычными для селениум-тестов проверками (текст, кликабельность, видимость).  

    Полезные ссылки про тестирование скриншотами:

    1. https://blog.rinatussenov.com/automating-manual-visual-regression-tests-with-python-and-selenium-be66be950196

    2. https://www.youtube.com/watch?v=crbwyGlcXm0


    Узнать подробнее о курсе «Python QA Engineer».

    Смотреть открытый вебинар по теме «Непрерывная интеграция с Jenkins».

    OTUS
    Цифровые навыки от ведущих экспертов

    Комментарии 0

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

    Самое читаемое