Игры, тестирование и спецификации

Привет. Я Леша Науменко, позиция моя в Plarium Kharkiv называется Unity Software Architect, и сегодня я расскажу о своем опыте применения спецификационного тестирования при разработке игр.

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

Более того, эти системы очень связаны между собой. Любому модулю вдруг может понадобиться знать о другом, который, на первый взгляд, никак к нему не относится. Характерным примером являются ачивки и квесты. Получить ачивку можно за что угодно, и, как следствие, система достижений хочет знать обо всем на свете.

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

Предпосылки, сложность и огромные числа

Сейчас в составе большой команды я работаю над мобильным RPG-проектом RAID: Shadow Legends, который вышел в релиз только в этом году. Примеры буду приводить из него. Механическая основа игры — коллекционирование и подбор героев для сражений против боссов в подземельях или других игроков на арене.

Правда, я тут не про саму игру, а про сложность ее тестирования хотел поговорить.

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

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

У каждого героя по три-четыре собственных скилла плюс сеты артефактов, добавляющие одну-две пассивные способности, плюс таланты (тоже пассивки, по 15 на героя).

Время умножать числа! В одной битве может одновременно оказаться:

10 [герои] × ((3 [скилы] + 1 [артефакты] + 15 [таланты]) × 3 [эффекты в скиле]) = 10 × (19 × 3) = 570 эффектов

Героев пока что около 370, и раз в несколько месяцев добавляется по 10–20 новых. Выбрать 10 героев с повторениями из 370, как мне подсказывают из Wolfram, можно

способами.

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

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

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

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

Разработчики, QA и автоматизация

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

И даже в командах, где юнит-тесты в целом работают (например, в нашей), QA им полностью не доверяют — и правильно делают.

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

Как насчет того, чтобы сделать сами требования тестами?

Спецификации, тесты и снова QA

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

Ребятам из SpecFlow эта мысль понравилась настолько, что они сделали это возможным для .NET-среды и, как следствие, для Unity-геймдева. На самом деле не они это изобрели и, да-да, Cucumber и BDD существуют много лет, но впервые я познакомился с этим в геймдеве и именно со SpecFlow, поэтому похвалю их.

Познакомился благодаря господину TLK, с которым на тот момент работал. Именно ему пришла в голову отличная мысль, что спецификационное тестирование просто идеально подходит для игр.

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

Соответственно, и тесты хотелось бы описывать тем же простым и понятным языком: никаких классов, методов, менеджеров и дат. Только в этом случае QA будут их писать, поддерживать и доверять им. Получается ли так сделать? Давайте посмотрим на нашу попытку.

Код, паттерны и регулярность

Вот несколько примеров тестов, описывающих игровую ситуацию, — их называют сценариями.

Все в полном соответствии с каноном BDD:

  • Given описывает расстановку;
  • When — момент действия;
  • Then — ожидаемый результат.

Перед нами текст на английском. Ломанном английском, да, но все еще без всяких «внутренностей» кода. И этот текст каким-то техномагическим образом проверяет, что мы не ошиблись, и отображается в привычном тестраннере! Разве не замечательно?

Маленький дисклеймер для опытных создателей BDD-сценариев и знатоков Gherkin: я понимаю, что это не принципиально новая штука, но все же некоторый восторг она у меня вызывает, потому что именно в такие моменты понимаешь, зачем учился программировать :-)

Разберем, что же происходит. Разумеется, текст преобразуется в код. Разумеется, с помощью регулярок, которые мы все очень любим. Но, правда, очень простых.

И все, исчезла магия, что ли? Ничего, смотрим дальше.

Каждой конструкции соответствует метод. Методы пишут уже программисты, в отличие от текста сценария, который может написать фактически кто угодно. Конструкции могут иметь синонимы — как в примере, где обе формулировки делают одно и то же. Это сделано для более комфортного написания сценариев разными людьми.

Каждый метод из блока Given наполняет расстановку, поэтому следует делать их маленькими и помнить об SRP. When просто дергает нужные ниточки, чтобы привести тестируемый механизм в действие. Then содержит проверки и assert-ы из вашего любимого фреймворка для этого. Мы используем fluentassertions.

Блок Given здесь необходимо рассмотреть более внимательно. Объект расстановки нужно наполнять постепенно, с каждым прочитанным предложением дополняя его чем-то новым. Чувствуете, да? Да это же паттерн Builder буквально в классическом представлении.

Да, именно он. И чтобы упростить себе жизнь, очень важно реализовать его здесь. Пример кода для построения битвы в блоке Given:

Есть еще один нюанс. Мы хотим больше человеческого языка и меньше всяких чисел и айдишников. По возможности все, что можно назвать, следует назвать с помощью enum-ов. Даже такие банальности, как Ally или Second [slot], будут выглядеть лучше, чем раскиданные по сценарию числа. И вместе с тем уменьшат возможность оступиться при написании самих тестов: вы ведь не одно число написали, а целое слово, которое невольно прочтешь — и быстрее заметишь неправильность.

Примеры про слот героя:

Код, проектирование и «надо было заранее»

Есть ли какие-то специфические требования к проекту и его коду, чтобы можно было реализовать все это великолепие?

Хотелось бы сказать, что нет, можно просто брать и тестировать, но, к сожалению, так не бывает. По крайней мере, без преодоления гор костылей.

О тестируемости вашего модуля придется подумать заранее. Можно и не совсем заблаговременно, но тогда нужно быть готовым к рефакторингам, что иногда тоже вполне себе опция.

Известная и много раз озвученная вещь, которую тем не менее следует отметить: для тестируемости нужно писать модульно, хорошо помня о DI и SRP. Будучи часто произносимой банальностью, это соображение так же часто игнорируется. Но в нашем случае это не просто для абстрактной красоты и гибкости, а для дела.

В примере модуль битвы написан как черный ящик, принимающий на входе setup битвы и отдающий наружу список результатов.

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

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

Внедрение, поддержка — и, пожалуй, достаточно

Теперь, наконец, о сложном и наиболее важном — о внедрении в команду. Вот вы вдохновились (на что я надеюсь) и предложили руководству сделать фреймворк для спецификационного тестирования. Правильно расставили акценты и объяснили, почему это упростит всем жизнь в недалеком будущем. Прикинули, что для этого нужно сделать, дали приблизительный эстимейт и получили одобрение и время на работу.

И вот ваши тестовые тесты забегали и засветились зеленым. Вы с горящими глазами идете к QA-лиду сообщить, что у вас есть кое-что интересное для него. И после того как вы взахлеб расскажете о вашем новом техномагическом устройстве, вместо восторга, на который надеялись, вы можете встретить строгий взгляд, полный сомнения.

И на рассматриваемом этапе это будет справедливо. Это ведь вы знаете, что спецификации работают. Вы уверены в том, что они тестируют корректно и не являются ложноположительными или ложноотрицательными.

(Уверены ведь? :) На всякий случай вот цикл статей о том, как улучшить навык написания юнит-тестов. Те же подходы проверки устойчивости и качества тестов подойдут и для спецификаций.)

QA пока не доверяют им. Но вам нужно будет это исправить. Возьмите несколько особо неприятных для мануального тестирования тест-кейсов из их регрессионного чек-листа. Сделайте спецификационную версию этих тестов. После этого обязательно напишите несколько таких же тестов уже с QA. Проверьте тесты на ложность вместе с ними и сделайте так, чтобы результаты запуска тестов были у них перед глазами (CI и Slack-боты вам в помощь :-) Дайте им возможность пожить с этими тестами некоторое время.

Но недолго. Наведывайтесь, интересуйтесь, получайте фидбек и настаивайте на написании новых тестов. А лучше составьте план тестов, покрывающих определенный участок проблемных мест в проекте, и предложите его. В общем, работайте над внедрением и поддержкой. Это чуть ли не важнее, чем написать сам фреймворк, ведь если его никто не будет использовать, то какой смысл был создавать его? :-)

Наш опыт выглядел так. После создания фреймворка QA довольно быстро захотели избавиться от части мануальных проверок, которые связаны со взаимодействием эффектов и с некоторыми граничными случаями поведения AI.

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

Например:

Писали они их вне зависимости от того, готова ли функциональность, транслирующая конкретные формулировки в код.

Вместе с тем мы выделили одного разработчика, который раз в неделю при наличии свободного времени реализовывал порцию недостающей для тестов функциональности. Таким образом нам удалось сделать этот процесс неутомительным и не особо замедляющим разработку.

Всего с тех пор написано 704 теста, которые постепенно и итеративно были реализованы, а теперь страхуют проект от случайных ошибок при каждом CI-забеге.

Так что в целом освоить спецификационное тестирование у нас получилось.

Вот так и закончим. Буду рад ответить на вопросы и прочитать, как устроено автоматизированное тестирование у вас, особенно если вы делаете игры.

Спасибо, что дочитали. И хорошего чего у вас там за окном!

Похожие статьи:
Компания ASUS представила новый смартфон ZenFone Selfie (ZD551KL), ориентированный на поклонников автопортретов (селфи). Новинка обладает двумя...
По слухам, аппарат Vivo Xplay 5S мог бы стать первым смартфоном, объем оперативной памяти которого достиг бы 6 ГБ. Однако, скорее всего, эти...
Maртін Ендер — Senior Software Developer з Німеччини, співзасновник компанії Kagenova, що займається штучним інтелектом для створення...
2019-й минув, і цього року, окрім переліку найкращих статей, трішки розкажу про підсумки редакції DOU. Минулого року...
Старт курса в Киеве: — 18 декабряСтарт курса в Одессе — 18 декабряНабор в группу в Одессе и Киеве уже...
Яндекс.Метрика