QA без рутины: как мы автоматизировали регрессионное тестирование
Перенимаем опыт web-разработки.
Всем привет, меня зовут Александр Мастюгин, я работаю тестировщиком в студии Nord. В сфере IT бытует предубеждение, что работа тестировщиком — нудное и однообразное занятие. Но я с этим не согласен: на мой взгляд, это творческая, техническая и исследовательская деятельность. Чтобы выполнять эту работу хорошо, нужно погрузиться в задачу, понять все ее тонкости, сложности, разобраться, какие у нее есть подводные камни.
Но для справедливости нужно сказать, что скучный момент все же есть — это регрессия. Чтобы минимизировать ее роль в рабочем процессе и, соответственно, избавиться от рутины, мы в студии Nord решили автоматизировать регрессионное тестирование. В этом тексте я расскажу, что у нас получилось.
Автоматизация механики автобатлера
Мы занимаемся разработкой игры Hustle Castle. Это мобильный автобатлер с элементами экономической стратегии и RPG. В качестве движка используется Unity, а сервер написан на Java. Core-механика автобоя заключается в следующем: юниты игрока и врага противостоят друг другу, у всех персонажей есть особая экипировка, которая дает способности, а сам бой идет автоматически — пользователь может лишь кастовать заклинания и использовать таланты своего героя.
Еще в Hustle Castle есть замок с кучей комнат — там можно добывать ресурсы, крафтить предметы и так далее. Также в игре есть сетевые механики с кланами, развитием территорий, ареной и многим другим. И все это сосуществует друг с другом и подчинено общей логике.
С описанием Hustle Castle закончили, теперь можно переходить к более глубоким вещам. Геймплей в игре завязан на способностях юнитов. С технической точки зрения абилка — это некая сущность, у которой есть множество настроек: как и когда она будет активироваться, на кого она будет действовать, какой у нее эффект, какие другие способности она может активировать и так далее.
Поведение каждой абилки мы описываем в виде объектов Json. Способности бывают простые, а есть структурные — в них возможна последовательная или параллельная активация других абилок. Кроме того, есть разные типы способностей, например, счетчики, баффы, станы. В общем, получается очень много всевозможных настроек.
Все это учитывается алгоритмом автобоя. Если сильно упростить, то работает это так. На входе есть некоторые данные — это состояние наших бойцов: какие у них статы, какими абилками они обладают. Эти данные мы передаем в батл-калькулятор, который и занимается всеми вычислениями. На каждый свой шаг вычислений он сообщает, какое произошло событие, активировал ли он какую-то абилку, нанес ли урон. После полного цикла вычислений мы получаем результат боя.
Стоит отметить, что код батл-калькулятора есть как на клиенте, так и на сервере. Это нужно для валидации результатов боя — получается, что и клиент, и сервер, и батл-калькулятор смотрят на одни и те же данные.
Чтобы автоматизировать проверку автобоя, мы решили сделать кастомный клиент, который умеет по входящим данным генерировать запрос к серверу на получение изначального стейта боя. Автотесты используют этот клиент, получают стейт, отдают его в калькулятор, подписываются на событие нового шага вычислений и дальше проверяют условия на каждом шаге.
Для проверок механик боевой системы мы используем абилки, собранные специально для тестов. Проверки нацелены на механики, абилки с прода мы не проверяем тестами.
Этот подход мы также используем в нашем внутреннем продукте под названием «Прогонялка боев». Она нужна нашим геймдизайнерам для баланса боевой системы. Геймдизайнер может взять любые стейты игроков с продакшена, объединить в тестовые группы, по желанию заменить одни абилки на другие и запустить прогонялку на большом количестве боев. По итогу прогона геймдизайнер получает статистику схваток.
Это был первый шаг к автоматизации. Наши автотесты дают уверенность, что базовая логика батл-калькулятора не сломалась после действий геймдизайнеров и разработчиков. Это комплексная вещь, для которой, к сожалению, у нас нет юнит-тестов. Поэтому эти автотесты боевого калькулятора — наша единственная возможность удостовериться, что большая часть боевой системы работает стабильно.
Тесты на сервере и на клиенте
Чтобы пояснить, что будет происходить дальше, обратимся к теории — перед вами известная пирамида тестирования.
Суть пирамиды проста. Чем мы ближе к основанию, тем дешевле и быстрее тесты. И наоборот — чем выше, тем дольше и дороже.
Решение вроде бы очевидное — нужно использовать юнит-тесты как самые быстрые и дешевые. Но все немного сложнее. Чтобы разработчики могли писать юнит-тесты, у приложения должен быть определенный дизайн — оно должно быть тестируемым. И, к сожалению, у нас это есть не везде.
Для серверной логики почти отсутствуют юнит-тесты, а те, что есть — это компонентные тесты, которые в основном покрывают матчмейкинг, базовую логику режимов и фичей.
Если брать отдельно сервер, то у нас следующая ситуация. Интеграционных тестов у нас на сервере нет. Тесты API теоретически можно написать, поскольку клиент и сервер у нас общаются по протоколу protobuf. А значит, описание протокола есть, можно взять клиент и слать запросы. Но пока что мы держим эту идею в запасе.
Что же на клиенте. Там дела обстоят несколько трагичнее. Юнит-тестов нет, компонентных тоже. Так мы оказываемся на вершине пирамиды — нам остается тестировать наше приложение через UI. Большая часть нашей игры выглядит вот так: много кнопок, диалогов, поп-апов, снова диалогов, снова кнопок. Почти все элементы интерфейса живут на Canvas.
В качестве базового инструмента мы взяли open-source решение AltUnityTester — это драйвер, который предоставляет:
- Поиск объектов с помощью x-path
- Управление сценами
- Симуляцию input-методов (tap, scroll, drag-n-drop и так далее)
- Вызов методов и получение свойств game-object’ов
- Протокол взаимодействия через web-сокет, который позволяет добавить множество других команд
В итоге мы взяли Java, Allure, TestNG, решили применить паттерн Page-Object и начали писать тесты. Поначалу получалось очень здорово и классно. Мы написали примерно 10-15 базовых тестов, которые просто проходились по интерфейсу и что-то выполняли.
Однако очень быстро стало понятно, что наша кодовая база содержит ряд проблем, которые будут отзываться все сильнее и сильнее с ростом проекта. Первая была связана с селекторами. На скриншоте ниже приведен пример, как мы использовали Page-Object. Поля класса — селекторы, а методы содержали вызовы к драйверу и дополнительную логику.
Проблема заключалась не только в том, что это выглядело массивно, но еще и в том, что во все наши классы попало API AltUnity. И если разработчики в новой версии что-то поменяют, нам будет мучительно больно обновлятся.
Другая проблема заключалась в ответственности Page-Object’ов. Во-первых, внутри Page-Object’а мы напрямую дергали драйвер (привет, API!). Во-вторых, объекты могли выполнять накрученную логику. В-третьих, наши Page-Object’ы знали о других Page-Object’ах — то есть навигацией по объектам занимались они сами.
Еще одной проблемой стала инъекция зависимостей. Когда классов было немного, все было в порядке. Но с усложнением тестов нужно было подключать кучу зависимостей, а также держать в голове, какие вообще есть.
Большое количество зависимостей вызывает ненужные трудности: например, если в компанию придет новый человек и попробует написать автотест — ему нужно будет изучить все многообразие API, составить ментальную карту того, какие есть у нас классы и как они связаны, и приложить еще много усилий, чтобы погрузиться в процесс.
И последняя проблема, с которой мы столкнулись — это дублирование кода. Например, на картинке выше представлен метод «OpenShopAndBuyRoom», который является приватным для этого тестового класса, поэтому мы не можем применять его где-то еще. Но так как мы стремимся написать больше тестов, мы хотим как-то переиспользовать этот метод и он должен принадлежать какому-то классу.
Время остановится и подумать
Использование AltUnityTester и паттерна Page-Object сильно напоминает автоматизацию в разработке web-приложений. Там наши коллеги используют Selenium WebDriver. И если взять концепции из web и переложить на нашу предметную область, то мы получим:
- UnityDriver — взаимодействие с игрой
- Unity-Object — структурный паттерн для описания диалогов, экранов и сцен. Мы используем их только для описания структуры, а всей логикой занимаются степы
- Unity-Element — кнопки, картинки, диалоги, текст и так далее. В общем, все то, что есть на сцене в Unity, у нас — UnityElement
Мы подсмотрели в исходники WebDriver и фреймворка HTML Elements и смогли адаптировать код под наши нужды. Также мы воспользовались паттерном Steps, чтобы отделить логику тестов от UnityObject'ов. На выходе мы получили фреймворк, с помощью которого мы можем:
- Выделить сущности в отдельные классы (Button, Label, AbstractDialog и так далее)
- Задавать x-path элементов UI с помощью аннотаций @FindBy, а также вводить новые аннотации и расширения
- Создавать отдельные блоки элементов и переиспользовать в разных диалогах за счет поиска объектов в контексте другого объекта
- Создавать представления компонентов в Unity на стороне тестов (так как на объекте может быть несколько компонентов)
- За счет степов писать тесты в терминах бизнес-логики игры («Отрыть магазин», «Купить товар» и так далее)
- Код AltUnity находится глубоко в ядре, а драйвер спрятан за интерфейсом
Немного про степы — они соединяют наши тесты с Unity Objects. Как раз Unity Objects дают возможность кликнуть на элемент или передать какие-то данные из игры, а вся логика находится в степах. Это дает нам возможность писать тесты в терминах бизнес-процесса. Например, «На локации — открой казарму», «В казарме — проапгрейдь казарму», «Возьми юнита — перенеси его в казарму». А уже под капотом находится drag-n-drop, клики и все остальное.
Вторая особенность степов в том, что в дальнейшем их можно переиспользовать. И не только в рамках функциональных тестов. Например, недавно потребовалось реализовать прогон одного сценария на множестве разных стейтов игроков. Создали новый проект, подключили библиотеку со степами, несколько строчек кода для запуска сценария — задача выполнена.
Итак, ниже находится Unity Object. Помните, как выглядели наши селекторы? Они были страшно некрасивыми. Теперь же мы используем просто аннотации, в которых прописываем, как искать нужный элемент и все.
Так выглядит описание почти любого диалога у нас в проекте. При этом степам доступны кнопки с возможностью нажать на них, списки повторяющихся объектов, а также степы могут получить информацию со всего диалога (сколько у нас золота, какие слоты открыты или закрыты и так далее).
Инициализацией полей классов занимается Unity Element Loader — он получает определенный класс и драйвер. Согласно некоторой логике, мы создаем прокси-элементы для каждого поля в классе. И тем самым мы можем просто написать «Кнопка нажмись», хотя на самом деле система сначала найдет эту кнопку, информация об этом вернется обратно и только после этого будет отправлена команда «Нажать».
Ниже можно увидеть, что у нас есть степы для диалогового окна с квестами. И эти степы описываются уже в терминах самой игры.
Примерно так выглядят все тесты. Мы применяем лишь одну инъекцию самих степов. Отталкиваясь от нее, мы пишем в терминах бизнес-логики то, что хотим сделать. В итоге все выглядит достаточно аккуратно.
Планы на будущее
Наши основные планы — сделать еще больше тестов. Все эти усилия были направлены на удобное, простое и понятное расширение нашей кодовой базы. Но впереди у нас маячит проблема — многопоточность.
На данный момент тесты прогоняются в одном потоке для одного инстанса игры. Все работает хорошо, но это долго.
Чтобы справиться с этим, мы можем создавать несколько инстансов на нашем удаленном сервере. Или же можем собрать ферму из устройств и подключаться к ним. Но часть наших фич «Глобальны» и могут мешать прохождению тестов. Например, если открыт «Портал для фарма», то он открыт для всех. А при открытии или закрытии «Портала» есть нотификации, что могут появится в интерфейсе у параллельно идущего теста и тап произойдет случайно по нотификации, а не по нужному элементу.
Следующая вещь, которую мы хотели бы реализовать — это back-to-back тесты: это когда вы берете две версии приложения, запускаете один и тот же сценарий, и в определенный момент делаете скриншоты. А после сравниваете. Так вы можете проверить, не поехало ли что-то у вас, не появилась ли фича раньше времени и так далее.
На данный момент мы расширяем покрытие фич, у нас есть смоковый набор с хотя бы одним тестом на любой аспект игры, а также мы начали обучать коллег писать тесты.
Фреймворк для автоматизации тестирования — это такой же продукт. Он должен быть простым, понятным и легко расширяемым. При его проектировании не стоит забывать о паттернах и принципах разработки ПО, а также пренебрегать рефакторингом. Иначе спасение от регрессии станет вашей головной болью при сопровождении. Иногда стоит сделать шаг назад, чтобы завтра сделать три вперед.
Спасибо за очень прекрасную статью! Всегда интересно, как строится автоматизация в других командах.
Будет еще круче, если вы расскажет, как настроено CI, где и на чем бегают тесты. И насколько все это оказалось интересно разработчикам: контрибьютят ли они в проект, следят ли за репортами?
Сам занимаюсь этой темой уже достаточно давно. В прошлом году выступал с лекцией про автотесты для мобилок на Heisenbug 2021, как раз сегодня (это знак!) видео опубликовали:
https://youtu.be/Tj_1DavX8cI
Там в описании есть и ссылка на слайды, и даже на пример мини-фреймворка.
Вздохнул, что у нас в проекте еще не такое плохое тестирование сервера... Для клиента вообще нет никакого пока что
Проблема заключалась не только в том, что это выглядело массивно, но еще и в том, что во все наши классы попало API AltUnity. И если разработчики в новой версии что-то поменяют, нам будет мучительно больно обновлятся.Для меня тут основная проблема — хардкод иерархии. Если кто-то что-то в окне менял, то и тесты ручками постоянно правили?
Кто-то, что-то всегда может поменять и в любом случае придется менять ручками) Самое страшное, что может случиться - новый интерфейс для какой то фичи. Но если он новый, то описать объекты и степы к ним в любом случае придеться. А такое случается очень редко. Чаще всего может поменяться селектор, так его и в одном месте поменять. Чуть более не приятное, добавление какой то логики в юзер-кейс. Скажем между двумя действиями добавилось третье. Чтож, поправить степ и вклинить его в нужные места с помощью средств идеи - занудная, но выполнимая задача