[Перевод] Введение в юнит-тестирование в Unity

image


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

  • Что это такое
  • Его польза
  • Достоинства и недостатки
  • Как оно работает в Unity при использовании Test Runner
  • Как писать и выполнять юнит-тесты, которые будут проходить проверку


Примечание: в этом туториале предполагается, что вы знакомы с языком C# и основами разработки в Unity. Если вы новичок в Unity, то изучите сначала другие туториалы по этому движку.

Что такое юнит-тест (Unit Test)?


Прежде чем углубляться в код, важно получить чёткое понимание того, что такое юнит-тестирование. Если говорить просто, то юнит-тестирование — это тестирование… юнитов.

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

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

public string name = ""
public void UpdateNameWithCharacter(char: character)
{
    // 1
    if (!Char.IsLetter(char))
    {
        return;
    }

    // 2
    if (name.Length > 10)
    {
        return;
    }

    // 3
    name += character;
}


Что здесь происходит:

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


Этот юнит можно протестировать, потому что он представляет собой «модуль» выполняемой работы. Юнит-тесты принудительно выполняют логику метода.

Пример юнит-тестов


Как нам написать юнит-тесты для метода UpdateNameWithCharacter?

Прежде чем мы начнём реализовывать эти юнит-тесты, нужно тщательно продумать, что делают эти тесты, и придумать для них названия.

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

UpdateNameDoesntAllowCharacterAddingToNameIfNameIsTenOrMoreCharactersInLength

UpdateNameAllowsLettersToBeAddedToName

UpdateNameDoesntAllowNonLettersToBeAddedToName

Из этих названий тестовых методов видно, что мы действительно проверяем, выполняется ли «юнит» работы методом UpdateNameWithCharacter. Эти названия тестов могут показаться слишком длинными и подробными, но это нам на пользу.

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

20d1e70ed7231be0c0cfd5f6f082751c.png


Запуск игры


Откройте Crashteroids Starter project (скачать его можно отсюда), а затем откройте сцену Game из папки Assets / RW / Scenes.

0296fd31696c7e75eeea4ec626709277.jpg


Нажмите на Play, чтобы запустить Crashteroids, а затем нажмите на кнопку Start Game. Перемещайте космический корабль стрелками влево и вправо на клавиатуре.

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

c2fceac408455035e41f3c9c268ff448.jpg


Попробуйте немного поиграть и убедиться, что после столкновения астероида с кораблём появляется надпись Game Over.

0b555ffa69f33fb09fb17204197af889.jpg


Начинаем работу с Unity Test Runner


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

Чтобы писать тесты, сначала нужно узнать о Unity Test Runner. Test Runner позволяет выполнять тесты и проверять, проходятся ли они успешно. Чтобы открыть Unity Test Runner, выберите Window ▸ General ▸ Test Runner.

f48f31aa4fc16c486b2161c75ba772af.jpg


После того, как в новом окне откроется Test Runner, можно будет упростить себе жизнь, нажав на окно Test Runner и перетащив его на место рядом с окном Scene.

e1025b408f785453827a962ce33aff67.gif


Подготовка NUnit и папок тестов


Test Runner — это предоставляемая Unity функция юнит-тестирования, но она использует фреймворк NUnit. Когда вы начнёте работать с юнит-тестами серьёзнее, то рекомендую изучить wiki по NUnit, чтобы узнать больше. Про всё необходимое на первое время будет рассказано в этой статье.

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

В окне Project выберите папку RW. Посмотрите на окно Test Runner и убедитесь, что выбран PlayMode.

Нажмите кнопку с названием Create PlayMode Test Assembly Folder. Вы увидите, что в папке RW появится новая папка. Нас устроит стандартное название Tests, поэтому можно просто нажать Enter.

5f5be4df386c8f262c4a3c257b55319f.gif


Возможно, вам интересно, что это за две разные вкладки внутри Test Runner.

Вкладка PlayMode используется для тестов, выполняемых в режиме Play (когда игра выполняется в реальном времени). Тесты вкладки EditMode выполняются вне режима Play, что удобно для тестирования таких вещей, как пользовательские behaviors в Inspector.

В этом туториале мы будем рассматривать тесты PlayMode. Но когда освоитесь, можете попробовать поэкспериментировать и с тестированием в EditMode. При работе с Test Runner в этом туториале всегда проверяйте, что выбрана вкладка PlayMode.

Что находится в комплекте тестов?


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

Test Runner обходит все файлы классов тестов и выполняет юнит-тесты из них. Файл класса, содержащий юнит-тесты, называется комплектом тестов (test suite).

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

Подготовка тестовой сборки и комплекта тестов


Выберите папку Tests и в окне Test Runner нажмите на кнопку Create Test Script in current folder. Назовите новый файл TestSuite.

c640ab968dfd79a3c72e2a99bc5daa2c.gif


Кроме нового файла C# движок Unity также создаёт ещё один файл под названием Tests.asmdef. Это файл определения сборки (assembly definition file), который используется для того, чтобы показать Unity, где находятся зависимости файла теста. Это нужно, потому что код готового приложения содержится отдельно от тестового кода.

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

Чтобы код тестов имел доступ к классам игры, мы создадим сборку кода классов и зададим ссылку в сборке Tests. Нажмите на папку Scripts, чтобы выбрать её. Нажмите правой клавишей на эту папку и выберите Create ▸ Assembly Definition.

5b824c84ad6686493c41951751030b8d.jpg


Назовите файл GameAssembly.

21bbd6c968426c23ea7839dd9f747355.png


Нажмите на папку Tests, а затем на файл определения сборки Tests. В Inspector нажмите на кнопку плюс под заголовком Assembly Definition References.

4befc3d13ea67e5ec94b707d04582a3d.png


Вы увидите поле Missing Reference. Нажмите на точку рядом с этим полем, чтобы открыть окно выбора. Выберите файл GameAssembly.

72799057fe5ff62ba5c997d5ec2a7563.jpg


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

a16bf525984f2f23b819d9b2d0e37a2b.png


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

Пишем первый юнит-тест


Дважды нажмите на скрипт TestSuite, чтобы открыть его в редакторе кода. Замените весь код на такой:

using UnityEngine;
using UnityEngine.TestTools;
using NUnit.Framework;
using System.Collections;

public class TestSuite
{


}


Какие тесты нам нужно написать? Честно говоря, даже в такой крошечной игре, как Crashteroids, можно написать довольно много тестов для проверки того, что всё работает как надо. В этом туториале мы ограничимся только ключевыми областями: распознаванием коллизий и базовой игровой механикой.

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


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

private Game game;

// 1
[UnityTest]
public IEnumerator AsteroidsMoveDown()
{
    // 2
    GameObject gameGameObject = 
        MonoBehaviour.Instantiate(Resources.Load("Prefabs/Game"));
    game = gameGameObject.GetComponent();
    // 3
    GameObject asteroid = game.GetSpawner().SpawnAsteroid();
    // 4
    float initialYPos = asteroid.transform.position.y;
    // 5
    yield return new WaitForSeconds(0.1f);
    // 6
    Assert.Less(asteroid.transform.position.y, initialYPos);
    // 7
    Object.Destroy(game.gameObject);
}


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

  1. Это атрибут. Атрибуты определяют особые поведения компилятора. Данный атрибут сообщает компилятору Unity, что код является юнит-тестом. Благодаря этому он отобразится в Test Runner при запуске тестов.
  2. Создаём экземпляр Game. Всё остальное вложено в game, поэтому когда мы создадим его, в нём будет находиться всё, что нужно тестировать. В среде продакшена скорее всего все элементы не будут находиться внутри одного префаба. Поэтому вам потребуется воссоздать все объекты, необходимые в сцене.
  3. Здесь мы создаём астероид, чтобы можно было следить за тем, двигается ли он. Метод SpawnAsteroid возвращает экземпляр созданного астероида. Компонент Asteroid имеет метод Move (если вам любопытно, как работает движение, то можете взглянуть на скрипт Asteroid внутри RW / Scripts).
  4. Отслеживание исходной позиции необходимо для того, чтобы убедиться, что астероид сместился вниз.
  5. Все юнит-тесты Unity являются корутинами, поэтому нужно добавить мягкий возврат. Также мы добавляем шаг времени в 0,1 секунды, чтобы симулировать течение времени, за которое астероид должен был двигаться вниз. Если вам не нужно симулировать шаг времени, то можно вернуть null.
  6. Это этап утверждения (assertion), на котором мы утверждаем, что позиция астероида меньше исходной позиции (то есть он сдвинулся вниз). Понимание утверждений — важная часть юнит-тестирования, и NUnit предоставляет различные методы утверждений. Прохождение или непрохождение теста определяется этой строкой.
  7. Разумеется, никто не наругает вас за оставленный после завершения тестов беспорядок, но другие тесты могут из-за него закончиться неудачно. Всегда важно подчищать (удалять или сбрасывать) код после юнит-теста, чтобы при запуске следующего юнит-теста не оставалось артефактов, которые могли бы повлиять на этот тест. Нам достаточно просто удалить игровой объект, потому что для каждого теста мы создаём полностью новый экземпляр game.


Прохождение тестов


Отлично, вы написали свой первый юнит-тест, но как узнать, что он работает? Разумеется, с помощью Test Runner! В окне Test Runner раскройте все строки со стрелками. Вы должны увидеть тест AsteroidsMoveDown в списке с серыми кружками:

d4c5d405a1aa5aded204806790a07233.jpg


Серый кружок означает, что тест пока не выполнялся. Если тест был запущен и пройден, то рядом показывается зелёная стрелка. Если тест завершился с ошибкой, то рядом с ним будет отображён красный X. Запустим тест, нажав на кнопку RunAll.

d45da58c99e0db679578aabab6e5601b.jpg


При этом создастся временная сцена и будет запущен тест. После завершения вы должны увидеть, что тест пройден.

64016b19e01a68544a1644b61ffb5c57.jpg


Вы успешно написали первый юнит-тест, утверждающий, что создаваемые астероиды движутся вниз.

Примечание: прежде чем начать писать собственные юнит-тесты, вам нужно понять реализацию, которую вы тестируете. Если вам любопытно, как работает тестируемая вами логика, то изучите код в папке RW / Scripts.


Использование интеграционных тестов


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

Интеграционные тесты — это тесты, проверяющие как работают «модули» кода совместно. «Модуль» — это ещё один нечёткий термин. Важное отличие заключается в том, что интеграционные тесты должны тестировать работу ПО в настоящем продакшене (т.е. когда игрок по-настоящему играет в игру).

f018246f3658664a77104a1fa1dc045b.png


Допустим, вы сделали игру с боями, где игрок убивает монстров. Можно создать интеграционный тест, чтобы убедиться, что когда игрок убивает 100 врагов, открывается достижение («ачивка»).

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

Интеграционный тест будет симулировать игрока, убивающего 100 монстров, и проверять, разблокируется ли достижение. Он очень отличается от юнит-теста, потому что тестирует крупные компоненты кода, работающие совместно.

В этом туториале мы не будем изучать интеграционные тесты, но это должно показать разницу между «юнитом» работы (и то, зачем её юнит-тестируют) и «модулем» кода (и то, зачем его тестируют интеграционно).

Добавление теста в комплект тестов


Следующий тест будет тестировать конец игры, когда корабль сталкивается с астероидом. Открыв в редакторе кода TestSuite, добавьте под первым юнит-тестом показанный ниже тест и сохраните файл:

[UnityTest]
public IEnumerator GameOverOccursOnAsteroidCollision()
{
    GameObject gameGameObject = 
       MonoBehaviour.Instantiate(Resources.Load("Prefabs/Game"));
    Game game = gameGameObject.GetComponent();
    GameObject asteroid = game.GetSpawner().SpawnAsteroid();
    //1
    asteroid.transform.position = game.GetShip().transform.position;
    //2
    yield return new WaitForSeconds(0.1f);

    //3
    Assert.True(game.isGameOver);

    Object.Destroy(game.gameObject);
}


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

  1. Мы вынуждаем астероид и корабль столкнуться, явно задавая астероиду ту же позицию, что и кораблю. Это создаст коллизию их хитбоксов и приведёт к концу игры. Если вам любопытно, как работает этот код, то взгляните на файлы Ship, Game, и Asteroid в папке Scripts.
  2. Шаг времени необходим, чтобы сработало событие Collision физического движка, поэтому возвращается задержка в 0,1 секунды.
  3. Это утверждение истины, и оно проверяет, что флаг gameOver в скрипте Game принимает значение true. Флаг принимает значение true во время работы игры, когда уничтожается корабль, то есть мы тестируем, чтобы убедиться, что ему присваивается значение true после уничтожения корабля.


Вернитесь в окно Test Runner и вы увидите, что там появился новый юнит-тест.

4a8f52b2875ca90a37b74348435e3da5.jpg


На этот раз мы запустим вместо всего комплекта тестов только этот. Нажмите на GameOverOccursOnAsteroidCollision, а затем на кнопку Run Selected.

7e20f5fbd8b4a8a6ff849fea66c90ed1.jpg


И вуаля, мы прошли ещё один тест.

96602f869c59d8218407a7ce2e57df7d.jpg


Этапы настройки и разрушения


Вы могли заметить, что в двух наших тестах есть повторяющийся код: там, где создаётся игровой объект Game и где задаётся ссылка на скрипт Game:

GameObject gameGameObject = MonoBehaviour.Instantiate(Resources.Load("Prefabs/Game"));
game = gameGameObject.GetComponent();


Также вы заметите, что повтор есть в уничтожении игрового объекта Game:

Object.Destroy(game.gameObject);


При тестировании такое случается очень часто. Когда дело доходит до запуска юнит-тестов, то на самом деле существует два этапа: этап «настройки» (Setup) и этап «разрушения» (Tear Down).

Весь код внутри метода Setup будет выполняться до юнит-теста (в этом комплекте), а весь код внутри метода Tear Down будет выполняться после юнит-теста (в этом комплекте).

Настало время упростить нашу жизнь, переместив код setup и tear down в специальные методы. Откройте редактор кода и добавьте следующий код в начало файла TestSuite, прямо перед первым атрибутом [UnityTest]:

[SetUp]
public void Setup()
{
    GameObject gameGameObject = 
        MonoBehaviour.Instantiate(Resources.Load("Prefabs/Game"));
    game = gameGameObject.GetComponent();
}


Атрибут SetUp указывает, что этот метод вызывается до выполнения каждого теста.

Затем добавим следующий метод и сохраним файл:

[TearDown]
public void Teardown()
{
    Object.Destroy(game.gameObject);
}


Атрибут TearDown указывает, что этот метод вызывается после выполнения каждого теста.

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

public class TestSuite
{
    private Game game;

    [SetUp]
    public void Setup()
    {
        GameObject gameGameObject = 
            MonoBehaviour.Instantiate(Resources.Load("Prefabs/Game"));
        game = gameGameObject.GetComponent();
    }

    [TearDown]
    public void Teardown()
    {
        Object.Destroy(game.gameObject);
    }

    [UnityTest]
    public IEnumerator AsteroidsMoveDown()
    {
        GameObject asteroid = game.GetSpawner().SpawnAsteroid();
        float initialYPos = asteroid.transform.position.y;
        yield return new WaitForSeconds(0.1f);
  
        Assert.Less(asteroid.transform.position.y, initialYPos);
    }

    [UnityTest]
    public IEnumerator GameOverOccursOnAsteroidCollision()
    {
        GameObject asteroid = game.GetSpawner().SpawnAsteroid();
        asteroid.transform.position = game.GetShip().transform.position;
        yield return new WaitForSeconds(0.1f);

        Assert.True(game.isGameOver);
    }
}


Тестируем Game Over и стрельбу лазером


Подготовив упрощающие нашу жизнь методы настройки и разрушения, можно приступить к добавлению новых тестов, в которых они используются. Следующий тест должен проверять, что когда игрок нажимает New Game, значение gameOver bool не равно true. Добавьте такой тест в конец файла и сохраните его:

[UnityTest]
public IEnumerator NewGameRestartsGame()
{
    //1
    game.isGameOver = true;
    game.NewGame();
    //2
    Assert.False(game.isGameOver);
    yield return null;
}


Это уже должно казаться вам привычным, но стоит упомянуть следующее:

  1. Эта часть кода подготавливает этот тест к тому, что булев флаг gameOver должен иметь значение true. При вызове метода NewGame он должен снова присваивать флагу значение false.
  2. Здесь мы утверждаем, что bool isGameOver равен false, что должно быть справедливо при вызове новой игры.


Вернитесь в Test Runner и вы должны увидеть что там появился новый тест NewGameRestartsGame. Запустите этот тест, как мы делали это ранее, и вы увидите, что он успешно выполняется:

b639d971f3e2a10105d91bb3c94fdbb1.jpg


Утверждение о движении лазерного луча


Следующим тестом нужно добавить тест того, что выстреливаемый кораблём лазерный луч летит вверх (аналогично первому написанному нами юнит-тесту). Откройте в редакторе файл TestSuite. Добавьте следующий метод и сохраните файл:

[UnityTest]
public IEnumerator LaserMovesUp()
{
      // 1
      GameObject laser = game.GetShip().SpawnLaser();
      // 2
      float initialYPos = laser.transform.position.y;
      yield return new WaitForSeconds(0.1f);
      // 3
      Assert.Greater(laser.transform.position.y, initialYPos);
}


Вот что делает этот код:

  1. Получает ссылку на созданный лазерный луч, испущенный из корабля.
  2. Исходная позиция записывается, чтобы мы могли проверить, что он движется вверх.
  3. Это утверждение соответствует утверждению из юнит-теста AsteroidsMoveDown, только теперь мы утверждаем, что значение больше (то есть лазер движется вверх).


Сохраните файл и вернитесь в Test Runner. Запустите тест LaserMovesUp и понаблюдайте за его прохождением:

2cea152426f82ea491345ead3f0eaa94.jpg


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

Проверка того, что лазер уничтожает астероиды


Далее мы убедимся, что при попадании лазер уничтожает астероид. Откройте редактор и добавьте в конец TestSuite следующий тест, а потом сохраните файл:

[UnityTest]
public IEnumerator LaserDestroysAsteroid()
{
    // 1
    GameObject asteroid = game.GetSpawner().SpawnAsteroid();
    asteroid.transform.position = Vector3.zero;
    GameObject laser = game.GetShip().SpawnLaser();
    laser.transform.position = Vector3.zero;
    yield return new WaitForSeconds(0.1f);
    // 2
    UnityEngine.Assertions.Assert.IsNull(asteroid);
}


Вот как это работает:

  1. Мы создаём астероид и лазерный луч, и присваиваем им одинаковую позицию для срабатывания коллизии.
  2. Это особая проверка с важным отличием. Видите, что мы явным образом используем для этого теста UnityEngine.Assertions? Так происходит потому, что в Unity есть особый класс Null, отличающийся от «обычного» класса Null. Утверждение фреймворка NUnit Assert.IsNull() не будет работать в проверках Unity на null. При проверках на null в Unity, нужно явным образом использовать UnityEngine.Assertions.Assert, а не Assert из NUnit.


Вернитесь в Test Runner и запустите новый тест. Вы увидите радующий нас зелёный значок.

27580f1b206647e0a2fd09c43577ee8f.jpg


Тестировать или не тестировать — вот в чём вопрос


Решение придерживаться юнит-тестов — непростое решение, и к нему не стоит относиться легкомысленно. Однако преимущества тестов стоят вложенных усилий. Существует даже методология разработки, называющаяся разработкой через тестирование (Test Driven Development, TDD).

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

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

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


Тестирование может быть большим вложением усилий, поэтому стоит рассмотреть достоинства и недостатки добавления юнит-тестирования в ваш проект:

Достоинства юнит-тестирования


У юнит-тестирования есть множество важных плюсов, в том числе и такие:

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


Недостатки юнит-тестирования


Однако у вас может и не быть времени или бюджета на юнит-тестирование. Вот его недостатки которые нужно учесть:

  • Написание тестов может занять больше времени, чем сам код.
  • Плохие или неточные тесты создают ложную уверенность.
  • Для правильной реализации нужно больше знаний.
  • Возможно, важные части кодовой базы нельзя будет покрыть тестами.
  • Некоторые фреймворки не позволяют с лёгкостью тестировать частные методы, что может усложнить юнит-тестирование.
  • Если тесты слишком хрупки (их слишком легко не пройти по ошибочным причинам), то на обслуживание может уйти много времени.
  • Юнит-тесты не отлавливают интеграционные ошибки.
  • UI тестировать сложно.
  • Неопытные разработчики могут тратить зря время на тестирование не тех аспектов.
  • Иногда тестирование элементов с внешними зависимостями или зависимостями времени выполнения может быть очень сложным.


Тестирование того, что при уничтожении астероидов увеличивается счёт


Настало время писать последний тест. Откройте редактор кода, добавьте показанный ниже код в конец файла TestSuite и сохраните его:

[UnityTest]
public IEnumerator DestroyedAsteroidRaisesScore()
{
    // 1
    GameObject asteroid = game.GetSpawner().SpawnAsteroid();
    asteroid.transform.position = Vector3.zero;
    GameObject laser = game.GetShip().SpawnLaser();
    laser.transform.position = Vector3.zero;
    yield return new WaitForSeconds(0.1f);
    // 2
    Assert.AreEqual(game.score, 1);
}


Это важный тест, проверяющий, что когда игрок уничтожает астероид, счёт увеличивается. Вот из чего он состоит:

  1. Мы создаём астероид и лазерный луч, и помещаем их в одну позицию. Благодаря этому возникает коллизия, которая запускает увеличение счёта.
  2. Утверждение, что game.score теперь равно 1 (а не 0, как было в начале).


Сохраните код и вернитесь в Test Runner, чтобы запустить этот последний тест и проверить, проходит ли его игра:

e5dd4c0ee4ad72e21a180621c65c11ba.jpg


Потрясающе! Все тесты пройдены.

Куда двигаться дальше?


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

Из этого туториала вы узнали, что такое юнит-тесты и как писать их в Unity. Кроме того, вы написали шесть юнит-тестов, которые успешно прошёл код, и познакомились с некоторыми из плюсов и минусов юнит-тестирования.

Чувствуете себя уверенно? Тогда можно написать ещё множество тестов. Изучите файлы классов игры и попробуйте написать юнит-тесты для других частей кода. Подумайте над добавлением тестов для следующих сценариев:

  • Каждый тип астероида при касании корабля приводит к концу игры.
  • Запуск новой игры обнуляет счёт.
  • Движение влево и вправо для корабля действует правильно.


Если вы хотите повысить уровень своих знаний о юнит-тестировании, то стоит изучить внедрение зависимостей и фреймворки для работы с mock-объектами. Это может сильно упростить настройку тестов.

Также прочитайте документацию NUnit, чтобы узнать больше о фреймворке NUnit.

И не стесняйтесь делиться своими мыслями и вопросами на форумах.

Успешного тестирования!

© Habrahabr.ru