Найти в Дзене
Сделай игру

Зачем нужны тесты в разработке игр

Оглавление

Сама идея написания тестов мне всегда казалась избыточной. Не то, чтобы я отрицал их пользу, однако большую часть времени они были сущей профанацией: проверялись какие-то случаи, которые с высокой степенью вероятности будут проверены на правильность ещё во время разработки, либо писались для откровенно фантастических случаев. Я уж не говорю о том, что те, кто эти тесты писал, нередко, также как и я не понимали зачем тестировать "это"? Оттого, тесты часто и получались лишь для галочки. Но однажды всё поменялось.

Зоопарк тестирования
Зоопарк тестирования

Немного про не особо полезные тесты

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

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

Переписывать 10-20% тестов, иерархически зависящих друг от друга никому не улыбалось.

И хотя несколько лет эти тесты несколько раз-таки позволили выявить нарушение поведения, трудозатраты на их написание и поддержание явно превышали ту пользу, которую они давали.

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

Структурная модель прежде всего

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

К счастью, способ провести тестирование существует. Оговорюсь сразу, тестировать таким образом можно не всё; хотя не всё и тестировать нужно.

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

Смысл в том, чтобы все алгоритмы и состояния сделать изначально тестируемыми и проверять их, исходя из предположения, что если составные части системы работают верно, то и вся система работает верно. Либо же использовать разные уровни тестов, проверяющие работу приложения как на уровне низкоуровнего компонента, так и на уровне высокоуровневых бизнес-правил (всё как любит Роберт Мартин).

Как это работает: допустим, у нас есть пользовательский интерфейс с окном на экране, которое открывается кликом на кнопку 1, имеет цвет 2 и закрывается кликом на кнопку 3. Если исходить из соображения, что у нас есть состояние экрана и функции-обработчики кнопок, то мы можем провести это тестирование исключительно на уровне функций или классов, отслеживая показатели состояния:

  • вызвали метод кнопки 1
  • взяли структуру окна и проверили, что поле цвета соответствует цвету 2
  • вызвали обработчик кнопки 3
  • убедились, что состояние видимости окна изменилось

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

Архитектурые требования для тестов

Увы, чтобы использовать такой подход, необходимо всегда отделять данные и правила от реализаций. Легко протестировать функцию, к которой есть прямой доступ, но что делать, если это вложенная функция вложенной функции? Она банально недоступна.

Именно поэтому и появляется первая сложность: разработка приложения изначально должна осуществляться с прицелом на будущее тестирование.

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

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

Впрочем, плюсов от такого, немного избыточного, подхода, тоже предостаточно.

Тесты для отладки

Если структуры данных и методы, управляющие ими, можно выделить в отдельные модули (или компоненты) - они становятся более удобны для тестирования. И, более того, это даёт значительный простор для обнаружения своих собственных ошибок.

Случай с неправильными вычислениями

Был у меня случай - писал несложную игрушку: дело несложное, привычное. Код, постепенно, разрастался.

Но когда всё было готово и заработало - всё работало неверно. Код выглядел правильным, собирался без проблем, но результат был неудовлетворительным.

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

Короче говоря, тесты, на которых я хотел сэкономить время, в конечном итоге сэкономили время мне. Дополнительный приятный момент заключался в том, что исходный код стал более прозрачным и понятным.

Тесты для проверки правильности работы

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

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

Думаю, вы уже поняли, что масштаб проблемы сильно больше, нежели просто тесты; тут речь идёт о глубокой проработке структуры данных, состояний, конечных автоматов приложения и многого другого.

Затея непростая, однако если стоит задача выпустить стабильный продукт, неизбежная.

Когда можно не писать тесты

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

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

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

Ещё можно пропустить для тех областей, которые будут однозначно проверяться тестировщиками, но не содержат при этом каких-либо непредсказуемых моделей поведения. Например, вход в приложение, который нельзя пропустить.

Хотя это, конечно, обобщённые советы. Руководствоваться надо, разумеется, здравым смыслом и уместностью тестов.

Заключение

Итак, тесты - это хорошо, если они не превращаются в повинность и профанацию, так как:

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