Как стать автором
Обновить
1317.62
OTUS
Цифровые навыки от ведущих экспертов

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

Время на прочтение5 мин
Количество просмотров15K

Для будущих студентов курса «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».

Теги:
Хабы:
Всего голосов 11: ↑10 и ↓1+12
Комментарии0

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS