Часто ли вы сталкивались с ситуацией, когда тесты падали из-за небольшой разницы между скриншотами? Например, не успел исчезнуть спиннер, не докрутился скроллбар, уведомление исчезло чуть быстрее, чем нужно и так далее.
Часто ли вы сталкивались с ситуацией, когда тесты падали из-за того, что на скриншоте появился сторонний элемент, который выпустила другая команда? Часто ли вы сталкивались… с самыми разными причинами, из-за которых приходится сидеть и анализировать фейлы скриншотных тестов?
Меня зовут Александр Гончар, я инженер по обеспечению качества в Т-Банке. Хочу поделиться опытом, как избавиться от скриншотных тестов.
От чего зависят скриншотные тесты
Скриншотное тестирование очень популярно: что может быть проще, чем сравнить актуальный скриншот с эталонным.
Работая много лет со скриншотными тестами, я сталкивался с самыми разнообразными проблемами, из-за которых такие тесты падали. Я попробовал обобщить факторы, от которых зависят скриншотные тесты:
Хранилище: доступы на чтение или запись в CI локально и у коллег; количество выделенного места, сетевой доступ, денежные затраты.
Различия между средами запуска — локально и в образе CI: ОС, версия браузера и его настройки, региональные настройки, часовой пояс, разрешение экрана, шрифты и так далее.
Изменения кода в вашей и сторонних командах: стили, новые общие компоненты, новая функциональность, затрагивающие сразу несколько продуктов или страниц, рефакторинг, дизайн;
Необходимость обновлять эталоны при валидных изменениях функциональности, дизайна, браузера и так далее.
Эти проблемы приводили к тому, что на анализ, перезапуск тестов, обновление эталонов тратилось много времени. С этим нужно было что-то делать.
Чтобы решить, нужно ли дальше продолжать работать со скриншотными тестами, мы решили выяснить:
плюсы отказа от скриншотных тестов;
что проверяется скриншотными тестами;
как работает компиляция в Angular и рендеринг в браузере.
Результаты исследования
Плюсы отказа от скриншотного тестирования:
Скорость выполнения тестов. Отдельный тест выполняется быстрее, так как нет действий со скриншотами: сохранение, вычитка, сравнение, обновление.
Например, несколько замеров времени прохода простого теста в одинаковой среде показали, что тесты без скриншотов выполняются быстрее:
Время прохода скриншотного теста | 2,2 с | 2,1 с | 2,4 с | 2,1 с | 2.1 с |
Время прохода теста без скриншота | 1,7 с | 1,7 с | 1,8 с | 1,8 с | 1,6 с |
Скорость прохода в CI: снижение времени выполнения тестов положительно сказывается на времени прохождения, например, MR.
Скорость доставки фичи заказчику: улучшается лид-тайм, заказчик и бизнес счастливы.
Не нужно тратить время на анализ упавших тестов — например, если тест упал из-за небольшой разницы между скриншотами.
Не нужно обновлять эталон.
Не нужно тратить ресурсы на перезапуск упавших тестов.
Тестировщики могут уделить больше времени тестированию, процессам обеспечения качества, повышению квалификации.
Что мы проверяем, используя скриншотное тестирование. Если взглянуть на скриншотный тест, увидим, что он проверяет идентичность актуального скриншота эталонному. Тест не проверяет, корректно ли отработала логика, например подсчет кэшбэка. Тест не проверяет, увидит ли пользователь необходимые данные, например сумму на счете или уведомление.
Можно провести аналогию, что это как использовать UI-логин в e2e-тесте и быть зависимым от него и от команды, которая разрабатывает механизм авторизации. Зачем тестировать логин, если тест должен проверять что-то другое?
Процесс обеспечения качества предполагает, что при тестировании мы оцениваем, как будет тестироваться ПО, какие проверки на каком уровне или этапе будут проведены.
Необходимо понимать, что нам нужно тестировать свой продукт, а не логику, элементы дизайна или компоненты, например, другой команды.
Логичный вопрос: а как же проверка дизайна, верстки, что она не поехала, что иконки и картинки корректно отображаются, что данные отображаются на странице, рендеринг в браузере, в конце концов?
Рассмотрим этот вопрос на примере приложения на Angular.
Компиляция в Angular. Историческая справка: начиная с Angular 9, используется AOT-компиляция. Она более надежна, чем JIT-компиляция, которая использовалась до этого. AOT-компиляция состоит из трех этапов: анализ, генерация кода и валидация, что позволяет избежать ошибок свойств и методов компонентов и сервисов в шаблонах.
В режиме AOT компиляция происходит в момент сборки приложения до загрузки в браузере, при этом HTML- и CSS-файлы включаются в файлы JavaScript в строковом виде и есть обнаружение ошибок при сборке, что снижает шанс их попадания в приложение.
Стремится к нулю шанс, что верстка, статичный текст, стили и прочие элементы будут с ошибками, хотя они могут возникнуть из-за ошибок в самом Angular или ошибок разработки. Это ошибки, которые прошли ревью кода, не были замечены на фиче-ветке, тестовом стенде, препроде и так далее.
Рендеринг в браузере выглядит так:
Браузер скачивает, читает и парсит исходники. При этом формируются DOM из HTML-документа и CSSOM, загружаются стили.
Формируется дерево рендеринга — это набор объектов рендеринга или фреймов. Они содержат соответствующие им DOM-объекты и стили.
Выполняются layout- и paint-процессы, когда для каждого объекта рассчитывается положение на странице, а затем происходит отрисовка.
Благодаря тому, как Angular подготавливает необходимые данные для рендеринга в браузере, вероятность проблем при рендеринге также стремится к нулю.
Уровень проверок. Возникал ли у вас вопрос, почему та или иная проверка покрыта e2e-тестом? Где пишется тест, если на проде был найден баг: добавляете ли e2e-тест, интеграционный тест или идете к разработчикам, чтобы они написали юнит-тест? SLT и пирамида тестирования приходят на помощь, чтобы ответить на эти вопросы.
Для примера возьмем пару скриншотных тестов, которые проверяют, что пользователь видит страницу с одним или несколькими руководителями, с заголовком «Руководитель <имя руководителя>», где отображается имя руководителя и соответствующая иконка, в зависимости от статуса, например, проверки.
Могут возникнуть вопросы: как происходит получение данных, отображаемых на странице? Это функция бэка или фронта? Написаны ли на эту функцию и ее логические пути юнит-тесты? Какие юнит-тесты есть на фронтовый компонент, который рисует нужный текст и иконку в зависимости от полученных данных?
Отвечая поочередно на эти вопросы, видим, что нам не просто не нужен скриншотный тест, нам не нужен даже e2e-тест: все необходимые проверки можно реализовать на нижних уровнях. Логику получения данных руководителя можно покрыть юнит-тестами как на бэке, так и на фронте. Отображение текста и иконки можно покрыть юнит-тестами на фронте.
В итоге благодаря современным браузерам и компиляторам не нужно:
тестировать скриншотами статичный текст, статичные иконки и другие статичные элементы, так как они практически всегда будут на странице;
тестировать скриншотами элементы, которые отображаются в зависимости от тех или иных условий: ответ от АПИ, выполнение функции на фронте, выполнение условия, например ngIf.
Перенос проверок в интеграционные и юнит-тесты сильно снижает вероятность регрессионных ошибок, так как тестирование на ранних этапах гораздо быстрее отлавливает потенциальные проблемы, исправить которые очень дешево. Те же юнит-тесты запускаются быстро, практически на постоянной основе и реагируют на изменения кода.
Приведу несколько примеров юнит-тестов на фронте, проверяющих отображение разных данных, при этом в начале теста используется метод, в котором в компонент передается один или несколько руководителей.
Единственный руководитель:
it('Должен вернуть тексты для единственного руководителя', () => {
defineDirectorsAndInitializeFixture(singleDirector);
const headerElem: HTMLElement = fixture.nativeElement.querySelector('[automation-id=director-header]');
const headerTextElem: HTMLElement = fixture.nativeElement.querySelector('[automation-id=director-text]');
expect(headerElem.textContent?.trim()).toBe('Руководитель');
expect(headerTextElem.textContent?.trim()).toBe(
'Укажите актуальные данные руководителя для продолжения работы с заявкой',
);
});
Несколько руководителей:
it('Должен вернуть тексты для нескольких руководителей', () => {
defineDirectorsAndInitializeFixture(severalDirectors);
const headerElem: HTMLElement = fixture.nativeElement.querySelector('[automation-id=director-header]');
const headerTextElem: HTMLElement = fixture.nativeElement.querySelector('[automation-id=director-text]');
expect(headerElem.textContent?.trim()).toBe('Руководители');
expect(headerTextElem.textContent?.trim()).toBe(
'Укажите актуальные данные руководителей для продолжения работы с заявкой',
);
});
Иконки руководителей:
it('Должен вернуть все типы иконок руководителя', () => {
defineDirectorsAndInitializeFixture(severalDirectors);
const connStoreDebugElem: DebugElement = fixture.debugElement;
const btnDebugElems: DebugElement[] = connStoreDebugElem.queryAll(By.css('[automation-id=director-icon]'));
const iconProperty = 'iconState';
expect(btnDebugElems[0].properties[iconProperty]).toBe('normal');
expect(btnDebugElems[1].properties[iconProperty]).toBe('error');
});
Процесс отказа от скриншотных тестов
Чтобы отказаться от скриншотов, нужно:
Собрать метрики по дефектам, связанные с фронтом. Разделить их на дефекты логики и дефекты дизайна или верстки, чтобы понять, как часто они возникают.
Примечание
если у вас таких дефектов окажется много, нужно выяснить и устранить причину их возникновения
Проанализировать тесты на тему того, что они проверяют.
Выделить набор критичных тестов, которые можно не трогать первое время, для душевного спокойствия.
Выделить проверки, которые выполняются скриншотами, и заменить их проверками без скриншотов. Например, обращение к элементу в DOM, вычитывание и проверка его значения и атрибутов
Выделить тесты, включая тесты из пункта выше, поверки которых можно реализовать в интеграционных или юнит-тестах.
Реализовать проверки из пункта выше в интеграционных и юнит-тестах.
Отказаться от тестов на e2e-уровне, чьи проверки реализованы в интеграционных или юнит-тестах.
Вернуться к набору критичных тестов и выполнить пункты 4—7 для них.
Дополнительно, если нашли баг на тестовом окружении или проде, при исправлении обязательно писать юнит или интеграционный тест.
Примечание
если вы вдруг поймали себя на том, что на найденный баг пишется e2e-тест, то устраняйте такую практику и покрывайте найденные проблемы в юнит или интеграционных тестах.
Регулярно отслеживать, не увеличилось ли количество багов на UI.
Вместо заключения
Мы избавились от скриншотных тестов, пересмотрели подходы к тестированию и уровню проверок в целом, ведь одно без другого не работает. Тестировщики больше вовлеклись в процессы и научились писать юнит-тесты на фронт.
Можно спросить: а зачем тестировщикам вникать в юнит-тесты, тем более уметь писать их, ведь традиционно это задача разработчиков? Мы стремимся к тому, чтобы тестировщики были вовлечены на всех этапах жизненного цикла ПО, в том числе и в юнит-тестировании. Тестировщик может прийти к разработчикам с проверками, которые нужно покрыть. Или реализовать их самостоятельно.
Уметь писать фронтовые юнит-тесты — хороший навык и это очень интересно.
Спасибо за уделенное время. На прощание — немного полезных ссылок: