Статья рассказывает о внедрении автоматизированных тестов для CROSSOUT.
В статье описан процесс создания тестового окружения и обоснован выбор инструментов, которые позволили нам справиться с требованиями проекта.
Сегодня я расскажу о нашем опыте создания и внедрения автоматизированных тестов в разработку Crossout.
Если вдруг кто-то вдруг еще не поиграл, вот пара слов о ней. Crossout - многопользовательская онлайн-игра в жанре постапокалиптического экшена.
У игры две основные части — конструктор, в котором мы создаем машины из деталей, которые нам удалось получить, и бой, где мы на этих машинах сражаемся (режимов боя у нас несколько, но на этом пока останавливаться не будем).
Таким образом, при тестировании приходится иметь дело с тем, что:
Много разных проверяемых элементов и высокий уровень неопределенности (столько-то штук деталей, машину из них можем собрать какую угодно)
Сложные проверяемые механики (проверка квестов, например)
Постоянное добавление контента (патч вышел - были добавлены или удалены детали и режимы)
Разные платформы (игра работает и на ПК, и на консолях)
Отсутствие готовых решений и фреймворков

Тестирование игровых проектов по праву считается областью ручного тестирования.
Crossout - это ММО проект, и обновления к нему выходят регулярно. И, разумеется, команде QA требуется регулярно проводить регрессионное тестирование проекта. Которое получается весьма объемным, с большим количеством тестов, на которые требуется много рабочего времени.
Кроме того, регрессионное тестирование это невероятно рутинная задача. А значит, имеет место человеческий фактор: повышенная утомляемость, потеря концентрации внимания - все это может повлиять на итоговое качество тестирования.
Поэтому возникает желание автоматизировать регрессионное тестирование.
Разумеется, идея не нова. Например, в веб-приложениях автоматизированное тестирование используется давно и широко. Существуют специальные фреймворки, которые значительно облегчают написание тестов для различных сайтов и вообще приложений, основанных на HTML.
Но игровая индустрия дело другое. Здесь известных примеров автоматизации тестирования гораздо меньше, и наработанных методов, не говоря уже о готовых решениях, просто нет.
Тем не менее, мы решились поставить такой эксперимент и попробовать автоматизировать регрессионное тестирование на проекте Crossout.

Разумеется, попробовать автоматизировать регрессионное тестирование довольно амбициозная и очень интересная задача сама по себе. Можно сказать, своего рода вызов. Но игровая индустрия - это таки бизнес, и перед тем как приступить к реализации идеи, стоит подумать о конкретных бизнес-результатах, которых мы хотим добиться в итоге.
Главной задачей любого бизнес-решения является повышение рентабельности. В случае тестирования повысить рентабельность возможно только снижением затрат. В игровой индустрии львиная доля производственных затрат - это зарплата сотрудников, поэтому смело можно считать затраты не в деньгах, а в человеко-часах. Если вспомнить о том, что проект выходит на различных платформах, и на каждой платформе требуется проводить сравнимый объем регрессионных тестов, общие затраты на регрессию видятся совсем немаленькими. Уменьшить же затраты человеко-часов в общем можно единственным способом: делать меньше тестов. Но уменьшая количество тестов, снижая таким образом тестовое покрытие, мы также снижаем и уверенность в качестве.
Автоматизированное тестирование обещает нам уменьшить количество тестировщиков, занятых в регрессионном тестировании, при сохранении уровня покрытия проекта регрессионными тестами.
А значит нет никаких причин не попробовать реализовать автоматизацию. Причины попробовать как раз есть!

Итак, принципиальное решение принято. Пришла пора думать о том, как мы будем его реализовывать.
При поиске решения надо было учитывать два класса критериев: критерии технические и критерии функциональные.
С технической точки зрения нам требовалось симулировать действия тестировщика.
Кроме того, мы сразу планировали работу на множестве платформ, в том числе на игровых консолях с их ограниченными ресурсами.
Поэтому решение не могло быть встроенным в клиент, было нужно что-то внешнее и имеющее возможность дистанционного управления клиентом игры.
Разрабатывать тесты планировалось силами команды тестировщиков, поэтому важным критерием отбора была простота языка написания скриптов.
По той же причине важно было, чтобы язык был широко известным, и, как следствие, к нему имелось большое количество доступных обучающих материалов и готовых решений.
Для облегчения процесса отладки тестов было решено, что лучше всего для наших целей подойдет интерпретируемый язык программирования. Связываться еще и с компилятором откровенно не хотелось.
В результате наш выбор пал на Python, который наилучшим образом удовлетворял всем критериям отбора.
В результате мы пришли к следующей конфигурации тестового окружения: управляющий компьютер, на котором исполняются тест-кейсы и подключенные к нему по сети тестовые платформы, на которых запускаются клиенты игры.
Довольно быстро выяснилось, что одним Python нам не обойтись. Да, на нем существует мощный и гибкий фреймворк для выполнения тестов PyTest, но одного его не хватит. Надо как-то симулировать нажатия на кнопки клавиатуры и работу мыши. Для решения этой задачи мы решили использовать библиотеку Python PyAutoGui.
В первых опытах все выглядело очень и очень неплохо. Курсор бегал по экрану, кнопки нажимались, тесты выполнялись. Но после сборки отдельных кейсов в более-менее длинные цепочки начались непонятные проблемы. Время от времени нажимаются не те кнопки, которые должны. Или кнопки не нажимаются. Или вместо клавиатуры некие действия пытается произвести мышь.
Выявить причины такого поведения мы так и не смогли. Поэтому было принято решение попросить программистов написать нам функции, которые позволяли бы нажимать кнопки, открывать списки, заполнять текстовые поля через консоль приложения, не используя периферию.
Игра поддерживает управление с геймпада, и про него тоже не следует забывать. Де-факто единственное решение тут это Xbox 360 Controller Emulator, для которого также существует соответствующая библиотека Python.
Некоторые тесты требуют распознавания объектов на экране по их внешнему виду. Тут нам помогает программа OpenCV, с которой Python также умеет работать.
Большое количество тестов создает большое количество информации, которую надо как-то обработать и представить пользователю. В этом нам помогает фреймворк Allure, разработанный компанией Яндекс.

Здесь уместно немного рассказать про Allure.
Разработчик этого фреймворка позаботился о том, чтобы подружить его с Python вообще и с PyTest в частности. Поэтому работать с ним достаточно просто. Мы размещаем в коде теста специальную разметку Allure, а по окончании работы используем Allure для генерации отчета.
На рисунке главная страница Allure. Здесь мы можем видеть:
сводный результат последнего тестирования,
историю тестов,
некоторые частные результаты.
В целом этого уже достаточно, чтобы быстро оценить комплексный результат проверки. Мы можем понять, какая часть тестов упала; примерно оценить, произошло падение из-за дефекта в тесте или в самом клиенте и\или коде теста. А также можем оценить, стало лучше или хуже по сравнению с предыдущими запусками

Кроме общей информации о результатах тестирования Allure предоставляет довольно подробную информацию о выполнении каждого из кейсов. Помимо результата и затраченного времени мы можем узнать о шагах выполнения кейса; можем увидеть код теста и call stack, если кейс отказал; можем посмотреть видеозапись теста.
Кроме того, Allure расставляет специальные флаги на тест-кейсах: новый это отказ или нет, стабилен ли этот кейс или он то срабатывает, то отказывает, и другое. Разумеется, доступна и сортировка кейсов, и фильтрация.
Как же Python управляет клиентом игры? Процесс отправки команд в клиент игры достаточно прост:
Pytest отправляет команду через TCP\IP сокет клиенту.
Клиент принимает команду через свой сокет и транслирует её в консоль.
Ну а дальше консоль выполняет команду, которая, как правило, является скриптом Lua.

Если консольная команда предполагает какой-то вывод, то этот вывод тем же путем возвращается в Pytest.
В идеальном случае все достаточно просто. Если для выполнения кейса нам требуются какие-то действия со стороны клиента, мы просто отправляем в удаленную консоль соответствующую команду и получаем нужный результат. Однако на практике быстро выяснилось, что для некоторых действий готовых функций в игре просто нет. Например, нет в Lua функции для получения свойств определенного виджета, ведь такую информацию скрипт Lua получает из кода C++.
Разумеется, мы заказывали программистам написание нужных нам функций. Но это дело не очень быстрое зачастую приходилось искать альтернативные решения. Например, сначала у нас не было функции, которая бы вводила текст в поле ввода. Поэтому нам приходилось использовать эмуляторы геймпада, клавиатуры, мыши.

Тут уместно обсудить важный вопрос: какой способ управления клиентом игры должен быть приоритетным: через виртуальные органы управления (клавиатура, мышь, геймпад) или через вызов функций в консоли?
Нам кажется, что более правильный способ это виртуальная периферия. Но практика показала, что широкое использование таких средств отрицательно влияет на стабильность тестов.
Попытка на 100% использовать эмуляторы сталкивается с теми проблемами, про которые я говорил чуть раньше.
Поэтому пришлось искать компромисс. Было принято решение, что эмуляторы будут использоваться только в специальных тест-кейсах, направленных на проверку управления игрой. В остальных кейсах будет применяться способ управления через удаленную консоль.
Кроме отсутствия некоторых функций в Lua, внедрение автотестов встретило и другие трудности. Перечислю некоторые из них.
Измерение урона. Урон в Crossout может иметь довольно сложную механику. Учитывая, что машина в игре не монолит, а состоит из некоторого количества деталей, каждая из которых имеет собственную прочность. Орудия же имеют разброс и могут поражать разные детали; некоторые орудия наносят урон в объеме, разделяя его между различными деталями, причем неравномерно. И это не весь перечень факторов, способных повлиять на результат выстрела. Следствием этого является то, что наносимый при одинаковых условиях выстрела урон подвержен случайным флуктуациям, величина которых может быть довольно значительна. Для борьбы с последствиями сложной механики урона пришлось использовать в качестве мишеней специально подготовленные машины и отключить различные механики, типа разброса при стрельбе.
Измерение времени также затруднено. У нас нет способа точно определить начало того или иного события. Ведь информация о нем поступает через TCP/IP, что гарантирует наличие некоторого лага. Из-за этого точность измерения временных интервалов относительно низкая. И если нас интересуют очень маленькие временные интервалы (например, время между запуском отдельных ракет в залпе), то результаты получаются с немаленькими и случайными по величине и знаку флуктуациями. Чтобы нивелировать эту проблему, приходится тест многократно повторять в надежде, что в один из повторов рандом не навредит очень уж сильно.
В некоторых случаях приходится прибегать к косвенным измерениям. Например, есть перки, которые на пару секунд продлевают работу щита. Надежно измерить столь малые временнЫе интервалы мы не можем. Проблему решаем, некоторое фиксированное время обстреливая цель непрерывным потоком пуль. Проведя две серии - с обычным и усиленным щитом - по сумме нанесенного урона мы можем судить о том, применился к щиту перк или нет.
Измерение пройденного пути также сталкивается с подобными трудностями. Казалось бы, почему? Точно мы не знаем, предполагаем это также может быть связано с тем, что команды от управляющего ПК проходят с задержкой, и величина этой задержки не является константой. Ну и боремся с рандомом уже упомянутым способом многократного повторения.
Отдельной проблемой является состояние клиента игры перед началом теста. Зачастую бывает так, что запуск отдельного кейса стабильно успешен, но этот же кейс в составе цепочки ведет себя нестабильно. Зачастую неудача одного из предыдущих тестов может сказываться на успехе многих последующих кейсов. Бороться с таким явлением получается только путем очень аккуратной подготовки и финализации каждого тест-кейса.
Множественность платформ вносит свои нюансы. Мало того, что для каждой платформы нужен свой аккаунт в игре, надо внимательно следить и за вспомогательными аккаунтами, а также названиями кланов и т.п. вещами, которые используются в тестах мультиплеера, клановой системы, выставок и других типов взаимодействия между игроками.
UI Crossout построен на системе виджетов, объединенных в единую древовидную структуру. Точно идентифицировать виджет можно только по пути к нему в дереве. Но время от времени дизайнеры UI вносят разные изменения, которые изменяют структуру дерева виджетов и, как следствие, “ломают” пути к виджетам. Разумеется, подобные изменения как правило не видны игрокам, но способны поломать автотесты. Избежать подобного нельзя в принципе, поэтому приходится время от времени корректировать пути. Разумеется, пути к наиболее часто используемым виджетам вынесены в константы, что позволяет оперативно и безопасно вносить необходимые изменения во все тест-кейсы сразу.
В ряде случаев тест требует использовать несколько клиентов игры одновременно.
Как видим, сложностей в разработке немало. Одним из побочных эффектов этого является то, что за тест-кейсами приходится постоянно следить и их актуализировать. Но, как правило, это занимает не очень много времени. В большинстве случаев на правку уходит 1-3 часа.
У необходимости постоянно актуализировать кейсы есть и полезный бонус: при таком подходе мы в значительной мере избавляемся от эффекта пестицида, что не может не радовать.
(Эффект пестицида это ситуация, когда повторяющиеся по одним и тем же сценариям тесты перестают находить новые отказы).
Итак, тесты пройдены и пришла пора смотреть на результаты. При изучении отчета важно понять, истинный или ложный результат был получен.
В этом сильно помогают:
видеофиксация кейсов. Прямое наблюдение сильно облегчает понимание того, что произошло.
повторение кейсов, всех или атомарно. Это не дает 100% гарантии, но если из 10 повторов хотя бы раз был достигнут успех, значит, с высокой долей вероятности, этот кейс не сломан.
детализированная подготовка результатов тестов в скриптах Python (результат теста не просто True или False, а довольно подробное и структурированное описание результатов проверок). Для этого пришлось написать специальный класс, наследуемый от bool, но в результате при падении есть возможность получить информацию о содержимом важных переменных.
Автотесты работают уже больше года, и вполне можно подвести первые итоги работы.
С технической точки зрения главным результатом, безусловно, является то, что удалось снизить объем ручного тестирования на 37% и при этом расширить покрытие регрессионного тестирования на 51%. Таким образом, уверенность в качестве продукта серьезно увеличилась.

С точки зрения организации работы мы получили возможность сосредоточить усилия наших тестировщиков на задачах, требующих знаний, умений, креативности… в общем, человека.
С точки зрения бизнеса результат так же впечатляет: затраты рабочего времени на регрессионное тестирование удалось снизить почти на 30%.

Подводя итог, можно утверждать, что внедрение автоматизированного регрессионного тестирования позволило:
высвободить тестировщиков
как следствие, улучшить тестирование новых фич
увеличить покрытие проекта тестами
как следствие, увеличить уверенность в качестве продукта
Таким образом, внедрение автоматизации в тестировании оказалось со всех сторон выгодным решением. Есть потенциал развития, что обещает увеличить полезный эффект по мере роста покрытия и дальнейшего высвобождения персонала.
Будем продолжать и развивать!
