Чем дольше вы работаете над крупными проектами в Unity, тем больше вы убеждаетесь в том, что существуют целые области, которые имеют основополагающее значение для создания надежного долгосрочного проекта, которые вы, возможно, даже не рассматривали при работе с небольшими проектами. В частности, к ним можно отнести модульное тестирование (или юнит-тестирование), хотя оно сложное и иногда самую малость муторное. С помощью модульного тестирования, разбивая кодовую базу на небольшие логические блоки и проверяя, работает ли каждый из них на детальном уровне, вы обеспечиваете прочную основу для своего приложения и избегаете регрессии кода.
Но настроить модульные тесты и автоматизировать их в Unity может быть довольно сложно. По этому предлагаю вам сегодня обсудить, как создавать и автоматизировать модульные тесты в Unity, используя конвейер CI/CD, созданный с помощью Codemagic.
Итак, из этой статьи вы узнаете:
Почему автоматизированные модульные тесты необходимы для эффективной гибкой разработки.
С какими проблемами можно столкнуться при автоматизации модульного тестирования в Unity.
Как создать простую игру (понг), которую мы будем использовать в качестве примера проекта.
Как настроить модульные тесты для нашей игры.
Как настроить Codemagic для автоматизации модульного тестирования и сборки проектов Unity.
За дело!
DevOps и автоматизация
Слово “DevOps”, представляющее собой конкатенацию “development” (разработка) и “operations” (операции), — это расплывчатый термин, которое может описывать разные вещи: команду (DevOps Team), набор практик, философию… В целом, речь идет об использовании всего лучшего из мира разработки и IT для создания лучшего кода и выпуска релизов легким и непрерывным образом.
DevOps — это философия, которой, например, обычно следуют веб-разработчики. Когда вы работаете на крупной онлайн-платформе, вам необходимо регулярно выпускать новые версии и обновлять программное обеспечение для своих клиентов. Поскольку это онлайн-сервис, вам, по сути, необходимо актуализировать исходный код где-то на другом компьютере (все мы знаем, что любое облако — это просто чей-то другой компьютер) последними обновлениями, чтобы выкатить свой новый релиз.
И вы, естественно, хотели бы избежать серьезного бага в вашем релизе или блокировки вашей инфраструктуры из-за строки кода с ошибкой. А учитывая, как часто вы будете релизить что-то новое (будь то огромный рефакторинг или очень небольшое исправление багов), было бы намного лучше, если бы все тестирование на наличие ошибок и публикация выполнялись автоматически, верно?
В этом весь смысл одной из основных задач DevOps: непрерывной интеграции и непрерывной доставки, или CI/CD. Это ключевые процессы в традиционной разработке программного обеспечения, которые направлены на автоматизацию доставки вашего продукта вместе с инструментами контроля версий, а также на то, чтобы эта доставка не вызывала никаких проблем, не приводила к простоям и не требовала срочных исправлений.
Вот почему модульное тестирование является фундаментальной концепцией DevOps: оно является одним из ключевых звеньев в цепочке автоматизации, потому что оно гарантирует, что код, который вы релизите, правилен и не приведет к сбою продукта после релиза. Поэтому процессы CI/CD в качестве обязательного шага обычно где-то интегрируют модульное тестирование. Если модульные тесты не пройдены, процесс доставки прерывается, и версия с ошибками не релизится.
Unity и автоматизация
Проблема с базовым инструментом CI/CD Unity
Лично я считаю, что Unity не очень хорошо оптимизирован для автоматизации непрерывной доставки. Философия DevOps не так легко воплощается в жизнь при работе с этим движком.
Справедливости ради я должен отметить, что сейчас в движок Unity встроены специальные инструменты, а именно Unity Cloud Build, которые пытаются восполнить этот пробел и удовлетворить потребность в автоматизации проектов Unity. Но у них есть свои ограничения. В частности:
Unity Cloud Build не позволяет публиковать релизы. Довольно часто ваш конвейер CI/CD должен заканчиваться фактической доставкой вашего обновления на устройство пользователя или в интернет-магазин (например, Google Play или App Store). Unity Cloud Build не может этого сделать, и вам все равно придется позаботиться об этом финальном этапе самостоятельно!
Инструмент также довольно медленный: даже не смотря на то, что там есть некоторые варианты кэширования, на данный момент Unity Cloud Build явно не оптимизирован с точки зрения скорости.
Кроме того, Unity Cloud Build не так хорошо работает с Git (отчасти потому, что Unity больше полагается на Perforce, который обычно лучше подходит для обмена цифровыми активами… и менее популярен среди разработчиков!).
Наконец, Unity Cloud Build работает только с проектами Unity, поэтому, если ваш проект комбинирует Unity, скажем, с React Native или другим фреймворком, вы не сможете использовать для него Unity Cloud Build.
Все это делает Unity Cloud Build хорошим, но все еще не идеальным выбором CI/CD для Unity. Он поможет вам с автоматизацией только до определенного этапа, а это может выливаться в некоторые довольно “крайностных” заморочках…
Codemagic спешит на помощь
Если вы не хотите иметь дело с этими трудностями, хорошая альтернатива для Unity Cloud Build — это Codemagic!
Этот онлайн-инструмент позволяет быстро настроить CI/CD, подключив свои Git-репозитории и выполнив всего несколько шагов настройки. Он также также позволяет вам получить доступ к определенному оборудованию: с Codemagic вы можете легко собрать свою игру для Mac, iOS, Windows, Android и т. д. Этот сервис также хорошо масштабируется с точки зрения цены и значительно упрощает публикацию вашего проекта в App Store или Google Play.
Итак, сегодня я покажу вам, как с помощью Codemagic автоматизировать некоторые процессы на примере игры на Unity (это будет несложная реализация понга).
Простая реализация понга
Примечание: Код проекта этого примера доступен на GitHub! ????
Теперь, когда мы обсудили проблемы реализации CI/CD в Unity и их возможные решения, и в частности, важность модульного тестирования в рамках процесса автоматизации, давайте создадим небольшой проект на Unity в качестве базового примера, на котором мы затем и настроем автоматизацию с помощью Codemagic. Мы собираемся реализовать известную игру понг… с модульными тестами!
Пишем понг
Целью этого проекта не является изобретение совершенно новой игры и анализ геймдизайна. Напротив, я возьму очень известную игру понг и частично перепишу ее; затем я задействую модульное тестирование, чтобы убедиться, что оно работает корректно.
Основная идея в рамках реализации этой игры состоит в том, чтобы написать:
Класс C# PaddleManager для управления каждой ракеткой/прямоугольником (клавишами <W> и <S> для левой ракетки и стрелками вверх/вниз для правой ракетки); их движение будет ограничено полем зрения камеры.
GameHandler и GameManager, которые создают ракетки, отслеживают счет и респаунят мяч со случайной скоростью. Я решил вынести основные функции в GameHandler (который не является MonoBehaviour, а представляет из себя ванильный класс C#), чтобы упростить их запуск в моих модульных тестах!
ScoreTrigger, который применяется к двумерным триггерным коллайдерам на левой и правой границах экрана. Он проверяет, выходит ли мяч за пределы поля — если это так, то победитель получает одно очко, и мяч респаунится.
После запуска игра будет работать без остановки, поэтому я не буду готовить никаких дополнительных меню или функций во время паузы, а просто сосредоточусь на основных механиках игры в понг. Я буду полагаться на 2D-физику Unity для различных отскоков и коллизий и подготовлю сцену со следующими элементами:
Две ракетки (2D-прямоугольники с компонентом BoxCollider2D и скриптом PaddleManager).
Мяч (2D-круг с CircleCollider2D и компонентом Rigidbody2D).
Две физические стены вверху и внизу экрана (чтобы ограничить перемещение мяча не полем зрения камеры и чтобы он “отскакивал” от границ экрана).
Два триггера в левой и правой частях экрана (невидимые 2D-элементы, каждый из которых имеет компонент BoxCollider2D с флагом isTrigger и скриптом ScoreTrigger).
Установка пакета Unity Test Framework
Если вы хотите проводить модульное тестирование в Unity, вы можете воспользоваться удобными инструментами, которые входят в официальный пакет Unity Test Framework. Это модуль, созданный самой командой Unity, который предоставляет удобный интерфейс дебага и сокращения кода для простого написания модульных тестов на C#. Добавить его в свой проект очень просто — откройте окно диспетчера пакетов Unity, найдите модуль Test Framework и кликните “Install” (или же просто убедитесь, что он уже установлен).
Как только он будет добавлен в ваш проект, вы увидите, что у вас появилась новая опция, доступная в разделе Window под названием Test Runner:
Создание тестовых сборок и тестовых скриптов в Unity
В этом окне вы можете легко создавать новые сборки (assemblies), чтобы группировать тесты и получать нужные зависимости. После создания файла определения сборки вы также можете добавить новый тестовый скрипт на C# с несколькими тест-кейсами, нажав кнопку “Create Test Script”.
В моем случае я буду писать все свои тесты в трех тестовых классах: BallTests, PaddleTests и ScoreTests. Каждый класс будет иметь различные “детализированные” тест-кейсы для проверки различных функций.
Чтобы зарегистрировать метод C# для сеанса тестирования, мне просто нужно присвоить ему атрибут Test (для обычной функции) или UnityTest (для корутины). Метод должен содержать одно или несколько утверждений (с использованием класса Assert ) для непосредственного запуска тестов.
Степень детализации (т. е. количество утверждений) в одном тест-кейсе полностью зависит от вас, но в целом объем каждого тест-кейса должен быть как можно более ограниченным. Например, в моем BallTests у меня будет только один тест-кейс, который проверяет правильный респаун мяча, когда я использую свою функцию GameHandler.InitializeBall()
:
using NUnit.Framework;
using UnityEngine;
using Pong; // пространство имен моих игровых скриптов
namespace EditorTests
{
public class BallTests
{
private GameHandler _handler = new GameHandler();
[Test]
public void ShouldInitializeBall()
{
GameObject ballObj = new GameObject();
Rigidbody2D ball = ballObj.AddComponent<Rigidbody2D>();
_handler.InitializeBall(ball);
Assert.AreEqual(ball.transform.position, Vector3.zero);
Assert.AreNotEqual(ball.velocity, Vector2.zero);
}
}
}
Этот тест утверждает, что после ресета мяча верны две вещи: во-первых, мяч находится в центре экрана, и во-вторых, его скорость не равна нулю.
Класс ScoreTests различает подсчет очков для левого и правого игроков — каждая ситуация обрабатывается в своем собственном тест-кейсе:
using NUnit.Framework;
using Pong;
namespace EditorTests
{
public class ScoreTests
{
private GameHandler _handler = new GameHandler();
[Test]
public void ShouldScoreLeft()
{
_handler.scoreLeft = 0;
_handler.scoreRight = 0;
_handler.ScorePoint(true);
Assert.AreEqual(_handler.scoreLeft, 1);
Assert.AreEqual(_handler.scoreRight, 0);
}
[Test]
public void ShouldScoreRight()
{
_handler.scoreLeft = 0;
_handler.scoreRight = 0;
_handler.ScorePoint(false);
Assert.AreEqual(_handler.scoreLeft, 0);
Assert.AreEqual(_handler.scoreRight, 1);
}
}
}
Но интереснее всего класс PaddleTests!
Первые тест-кейсы связаны только с инициализацией игры — я проверяю, правильно ли созданы, размещены и настроены ракетки. Но следующие методы — это корутины (использующие UnityTest), которые имитируют небольшой промежуток времени, в течение которого я двигаю ракетку.
Поскольку обе ракетки работают одинаково, и различаются только входные данные, я не делаю различий между левой и правой и предполагаю, что могу ограничится проверкой только одной из них. Я запускаю четыре теста на левой ракетке:
ShouldMovePaddleUp()
иShouldMovePaddleDown()
просто утверждают, что функцииMoveUp()
иMoveDown()
изменяют положение ракетки.
Затем функции
ShouldKeepPaddleBelowTop()
иShouldKeepPaddleAboveBottom()
утверждают, что это движение ограничено границами камеры и не выходит за верхний и нижний края экарана.
Помимо утверждений, все эти методы следуют одной и той же логике: во-первых, я увеличиваю timeScale, чтобы тест выполнялся быстрее (это похоже на запуск вашей игры в ускоренном темпе); затем я моделирую ситуацию, которую хочу проверить; и, наконец, я возвращаю timeScale его нормальное значение 1.
В качестве примера, вот первый тест-кейс, ShouldMovePaddleUp()
:
using System.Collections;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
using Pong;
namespace EditorTests
{
public class PaddleTests
{
private GameHandler _handler = new GameHandler();
// ...
[UnityTest]
public IEnumerator ShouldMovePaddleUp()
{
// увеличиваем timeScale для быстрого выполнения теста
Time.timeScale = 20f;
(GameObject left, _) = _handler.CreatePaddles();
PaddleManager pm = left.GetComponent<PaddleManager>();
// двигаем ракетку вверх
float startY = left.transform.position.y;
float time = 0f;
while (time < 1)
{
pm.MoveUp();
time += Time.fixedDeltaTime;
yield return null;
}
Assert.Greater(left.transform.position.y, startY);
// сбрасываем timeScale
Time.timeScale = 1f;
}
// ...
}
Если вы хотите взглянуть на другие функции, вы можете сделать это в репозиторий GitHub ????.
Запуск Unity через консоль
Во-первых, очевидно, что мы не можем использовать пользовательский интерфейс Unity для запуска проверок программно в автоматизированном рабочем процессе. Вместо этого нам нужно полагаться на консольный функционал Unity для запуска наших тестов и интеграции нашей фазы модульного тестирования в наш конвейер CI/CD.
Чтобы лучше контролировать выполняемые нами модульные тесты и выходной код, мы собираемся написать собственную функцию запуска тестов и вызывать ее из командной строки.
Итак, давайте создадим рядом с нашими тестовыми скриптами новый класс C# Runner со следующим кодом:
using UnityEditor;
using UnityEditor.TestTools.TestRunner.Api;
using UnityEngine;
public static class Runner
{
private static TestRunnerApi _runner = null;
private class MyCallbacks : ICallbacks
{
public void RunStarted(ITestAdaptor testsToRun)
{}
public void RunFinished(ITestResultAdaptor result)
{
_runner.UnregisterCallbacks(this);
if (result.ResultState != "Passed")
{
Debug.Log("Tests failed :(");
if (Application.isBatchMode)
EditorApplication.Exit(1);
}
else
{
Debug.Log("Tests passed :)");
if (Application.isBatchMode)
EditorApplication.Exit(0);
}
}
public void TestStarted(ITestAdaptor test)
{}
public void TestFinished(ITestResultAdaptor result)
{}
}
public static void RunUnitTests()
{
_runner = ScriptableObject.CreateInstance<TestRunnerApi>();
Filter filter = new Filter()
{
testMode = TestMode.EditMode
};
_runner.RegisterCallbacks(new MyCallbacks());
_runner.Execute(new ExecutionSettings(filter));
}
}
Здесь используется Unity Test Framework для программного запуска тестов и запуска специального колбека, когда все тесты завершены.
Теперь мы можем запустить его в терминале, вызвав исполняемый файл Unity из командной строки. Путь немного отличается на компьютерах Windows и Mac. Вот пример для системы OS X:
/Applications/Unity/Hub/Editor/2020.3.20f1/Unity.app -batchmode -projectPath /path/to/your/project -nographics -executeMethod Runner.RunUnitTests -logFile unit_test_logs.log
Первая часть командной строки — это путь к исполняемому файлу Unity; -batchmode
и -nographics
предназначены для запуска редактора в консольном режиме без пользовательского интерфейса. Параметр -executeMethod
запустит нашу функцию напрямую, а параметр -logFile
укажет путь к выходному файлу лога выполнения.
Не забудьте изменить параметр -projectPath
на ваш собственный путь к проекту Unity!
Настройка Codemagic CI/CD
Последний шаг — интегрировать этот этап модульного тестирования в рабочий процесс Codemagic, чтобы он стал частью автоматизированного процесса CI/CD.
Важное примечание: На момент публикации этой статьи для сборок Unity требуется специальная учетная запись Codemagic, и вам необходимо связаться с Codemagic, чтобы получить ее. В противном случае шаги, описанные в этом руководстве по CI/CD не сработают, поскольку у вас не будет доступа к инстансам Mac Pro или Windows с установленным Unity! Вам также потребуется лицензия Pro Unity — вам нужно активировать лицензию во время рабочего процесса, чтобы сборка и публикация работали должным образом.
Codemagic полагается на приложения: каждый проект — это приложение, которое извлекает код из удаленного репозитория (GitHub, GitLab, Bitbucket или любого другого пользовательского). После того, как вы создали учетную запись Codemagic, вам нужно перейти в свою панель инструментов, где вы можете создать и настроить новое приложение:
Сначала вам нужно подвязать репозиторий Git, из которого вы хотите подтягивать код проекта:
Затем вы сможете настроить приложение немного больше, в частности, добавив некоторые переменные среды. Это безопасный способ добавления конфиденциальных данных, таких как учетные данные, без их фиксации в репозитории Git!
Как объясняется в документации Codemagic для создания приложений Unity, вам необходимо определить три переменные среды: UNITY_SERIAL, UNITY_USERNAME и UNITY_PASSWORD — обязательно добавьте их все в группу unity:
Следующим шагом будет добавление скрипта сборки C# в ваш проект как Editor/Build.cs для создания проекта программным путем вместо того, чтобы вручную прожимать “Build” на панели “Build Settings”:
using System.Linq;
using UnityEditor;
using UnityEngine;
public static class BuildScript
{
[MenuItem("Build/Build Mac")]
public static void BuildMac()
{
BuildPlayerOptions buildPlayerOptions = new BuildPlayerOptions();
buildPlayerOptions.locationPathName = "mac/" + Application.productName + ".app";
buildPlayerOptions.target = BuildTarget.StandaloneOSX;
buildPlayerOptions.options = BuildOptions.None;
buildPlayerOptions.scenes = GetScenes();
Debug.Log("Building StandaloneOSX");
BuildPipeline.BuildPlayer(buildPlayerOptions);
Debug.Log("Built StandaloneOSX");
}
private static string[] GetScenes()
{
return (from scene in EditorBuildSettings.scenes where scene.enabled select scene.path).ToArray();
}
}
Здесь я определил сборку только для компьютеров Mac, но в документации Codemagic показаны конфигурации сборки под другие платформы.
Одна из очень приятных особенностей Codemagic заключается в том, что вы можете делать все в файле определения рабочего процесса! Эта конфигурация определяется в codemagic.yaml, который вы помещаете в корень вашего проекта (рядом с основной папкой Assets/)! Поскольку наш рабочий процесс здесь довольно прост, идея состоит в том, чтобы просто связать последовательность shell-команд для выполнения различных шагов процесса CI/CD, например:
workflows:
unity-mac-workflow:
# Для создания Unity на macOS требуется специальный тип инстанса, доступный по запросу.
name: Unity Mac Workflow
environment:
groups:
# Добавьте переменные среды группы в пользовательский интерфейс Codemagic (в переменные Application/Team) - https://docs.codemagic.io/variables/environment-variable-groups/ps/
- unity # <-- (Includes UNITY_SERIAL, UNITY_USERNAME, UNITY_PASSWORD)
vars:
UNITY_BIN: /Applications/Unity/Hub/Editor/2020.3.20f1/Unity.app/Contents/MacOS/Unity
scripts:
- name: Activate License
script: $UNITY_BIN -batchmode -quit -logFile -serial ${UNITY_SERIAL?} -username ${UNITY_USERNAME?} -password ${UNITY_PASSWORD?}
- name: Run Unit Tests
script: $UNITY_BIN -batchmode -executeMethod Runner.RunUnitTests -logFile -nographics -projectPath .
- name: Build
script: $UNITY_BIN -batchmode -quit -logFile -projectPath . -executeMethod BuildScript.BuildMac -nographics
artifacts:
- "mac/UnitTestingPong.app"
publishing:
scripts:
- name: Deactivate License
script: $UNITY_BIN -batchmode -quit -returnlicense -nographics```
Как видите, в этом файле мы указываем шаги выполнения, а также тип инстанса для запуска рабочего процесса, загружаемые переменные среды, артефакты и т.д.
Часть environment позволяет нам “внедрить” все переменные среды, которые мы определили ранее в нашей группе “unity”, а также некоторые пользовательские переменные (например, UNITY_BIN). Блок scripts определяет различные этапы нашего рабочего процесса:
Сначала мы активируем нашу лицензию Pro Unity, чтобы иметь доступ ко всем необходимым инструментам.
Затем мы запускаем модульные тесты: если все проходит успешно (то есть, если у нас на выходе 0), мы переходим к следующему шагу; в противном случае рабочий процесс немедленно прерывается и ничего не публикуется.
Затем мы фактически встраиваем новую версию в приложение и сохраняем ее как артефакт сборки.
И, наконец, мы деактивируем лицензию и выполняем некоторую очистку.
Перед очисткой мы также можем добавить шаг публикации в сторы, как описано здесь.
В конце концов, мы создадим единственный артефакт (см. блок artifacts): сам исполняемый файл, нашу игру понг.
После того, как вы закомитили и запушили все это в свой репозиторий, просто вернитесь к дашборду Codemagic и запустите сборку своего приложения, выбрав этот рабочий процесс из списка:
Справа вы увидите различные этапы CI/CD:сначала будет инициализирована удаленная машина, затем будет извлечен код из вашего репозитория, а затем codemagic.yaml будут выполнены шаги из вашего конфигурационного файла
Если ваш рабочий процесс по какой-либо причине завершается с ошибкой с кодом выхода 1 (например, если модульные тесты не выполняются), процесс будет прерван:
В противном случае все отработает до конца и автоматически развернет ваше приложение:
Вы можете перейти к своим сборкам для проверки различных состояний сборок вашего рабочего процесса. Здесь вы увидите репозиторий и коммит, который использует каждая сборка, а также время, необходимое для завершения, и получите ссылку для загрузки артефактов, созданных в ходе рабочего процесса:
Заключение
Вуаля! Благодаря Codemagic мы автоматизировали наш процесс CI/CD в Unity, сдобрив его модульным тестированием, чтобы убедиться, что наш код в порядке и не вызывает регрессий или ошибок! Теперь вы можете развить его еще больше, добавив отчеты о тестировании или планирование рабочего процесса…
Надеюсь, вам понравилось это руководство! И, конечно же, не стесняйтесь делиться своими идеями по другим темам DevOps, по которым вы хотели бы, чтобы я сделал руководства по Unity! Вы можете найти меня в Slack Codemagic, на Medium или в Twitter.
Код примера проекта для этого руководства вместе с codemagic.yaml размещен на GitHub здесь.
Всех желающих приглашаем на открытое занятие «Практика в эмуляторе Android Studio, BlueStack, Git». Цель занятия: закрепить практику тестирования игры. Краткое содержание: тестирование игры на эмуляторе (BlueStack) и тестирование игры на Unity 3D. Результаты: верифицированная игра и отчет по тестированию. Регистрация доступна по ссылке.