Чем дольше вы работаете над крупными проектами в 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 для различных отскоков и коллизий и подготовлю сцену со следующими элементами:

  1. Две ракетки (2D-прямоугольники с компонентом BoxCollider2D и скриптом PaddleManager).

  2. Мяч (2D-круг с CircleCollider2D и компонентом Rigidbody2D).

  3. Две физические стены вверху и внизу экрана (чтобы ограничить перемещение мяча не полем зрения камеры и чтобы он “отскакивал” от границ экрана).

  4. Два триггера в левой и правой частях экрана (невидимые 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. Результаты: верифицированная игра и отчет по тестированию. Регистрация доступна по ссылке.

Комментарии (0)