- PVSM.RU - https://www.pvsm.ru -
Вам любопытно, как работает юнит-тестирование в Unity? Не знаете, что такое юнит-тестирование в целом? Если вы ответили положительно на эти вопросы, то данный туториал будет вам полезен. Из него вы узнаете о юнит-тестировании следующее:
Примечание: в этом туториале предполагается, что вы знакомы с языком C# и основами разработки в Unity. Если вы новичок в Unity, то изучите сначала другие туториалы по этому движку [1].
Прежде чем углубляться в код, важно получить чёткое понимание того, что такое юнит-тестирование. Если говорить просто, то юнит-тестирование — это тестирование… юнитов.
Юнит-тест (в идеале) предназначен для тестирования отдельного «юнита» кода. Состав «юнита» может варьироваться, но важно помнить, что юнит-тестирование должно тестировать ровно один «элемент» за раз.
Юнит-тесты необходимо создавать для проверки того, что небольшой логический фрагмент кода в конкретном сценарии выполняется именно так, как вы ожидаете. Это может быть сложно понять, прежде чем вы начнёте писать собственные юнит-тесты, поэтому давайте рассмотрим пример:
Вы написали метод, позволяющий пользователю вводить имя. Метод написан так, что в имени не допускаются цифры, а само имя может состоять только из десяти или менее символов. Ваш метод перехватывает нажатие каждой клавиши и добавляет соответствующий символ в поле name
:
public string name = ""
public void UpdateNameWithCharacter(char: character)
{
// 1
if (!Char.IsLetter(char))
{
return;
}
// 2
if (name.Length > 10)
{
return;
}
// 3
name += character;
}
Что здесь происходит:
Этот юнит можно протестировать, потому что он представляет собой «модуль» выполняемой работы. Юнит-тесты принудительно выполняют логику метода.
Как нам написать юнит-тесты для метода UpdateNameWithCharacter
?
Прежде чем мы начнём реализовывать эти юнит-тесты, нужно тщательно продумать, что делают эти тесты, и придумать для них названия.
Посмотрите на показанные ниже примеры названий юнит-тестов. Из названий должно быть понятно, что они проверяют:
UpdateNameDoesntAllowCharacterAddingToNameIfNameIsTenOrMoreCharactersInLength
UpdateNameAllowsLettersToBeAddedToName
UpdateNameDoesntAllowNonLettersToBeAddedToName
Из этих названий тестовых методов видно, что мы действительно проверяем, выполняется ли «юнит» работы методом UpdateNameWithCharacter
. Эти названия тестов могут показаться слишком длинными и подробными, но это нам на пользу.
Каждый написанный вами юнит-тест является частью комплекса тестов. Комплекс тестов содержит все юнит-тесты, относящиеся к логической группе функционала (например, «юнит-тесты боя»). Если любой тест из комплекта не проходит проверку, то её не проходит весь комплект тестов.
Откройте Crashteroids Starter project (скачать его можно отсюда [2]), а затем откройте сцену Game из папки Assets / RW / Scenes.
Нажмите на Play, чтобы запустить Crashteroids, а затем нажмите на кнопку Start Game. Перемещайте космический корабль стрелками влево и вправо на клавиатуре.
Для выстрела лазерным лучом нажимайте пробел. Если луч попадёт в астероид, то счёт увеличится на единицу. Если астероид сталкивается с кораблём, то корабль взрывается и игра завершается (с возможностью начать заново).
Попробуйте немного поиграть и убедиться, что после столкновения астероида с кораблём появляется надпись Game Over.
Теперь, когда мы знаем, как выполняется игра, настало время писать юнит-тесты, чтобы проверить, что всё работает как надо. Таким образом, если вы (или кто-то ещё) решите обновить игру, то будете уверены, что обновление не сломает ничего из работавшего раньше.
Чтобы писать тесты, сначала нужно узнать о Unity Test Runner. Test Runner позволяет выполнять тесты и проверять, проходятся ли они успешно. Чтобы открыть Unity Test Runner, выберите Window ▸ General ▸ Test Runner.
После того, как в новом окне откроется Test Runner, можно будет упростить себе жизнь, нажав на окно Test Runner и перетащив его на место рядом с окном Scene.
Test Runner — это предоставляемая Unity функция юнит-тестирования, но она использует фреймворк NUnit. Когда вы начнёте работать с юнит-тестами серьёзнее, то рекомендую изучить wiki по NUnit [3], чтобы узнать больше. Про всё необходимое на первое время будет рассказано в этой статье.
Для запуска тестов нам сначала нужно создать папку тестов, в которой будут храниться классы тестов.
В окне Project выберите папку RW. Посмотрите на окно Test Runner и убедитесь, что выбран PlayMode.
Нажмите кнопку с названием Create PlayMode Test Assembly Folder. Вы увидите, что в папке RW появится новая папка. Нас устроит стандартное название Tests, поэтому можно просто нажать Enter.
Возможно, вам интересно, что это за две разные вкладки внутри 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.
Кроме нового файла C# движок Unity также создаёт ещё один файл под названием Tests.asmdef. Это файл определения сборки (assembly definition file), который используется для того, чтобы показать Unity, где находятся зависимости файла теста. Это нужно, потому что код готового приложения содержится отдельно от тестового кода.
Если у вас возникла ситуация, когда Unity не может найти файлы тестов или тесты, то убедитесь, что существует файл определения сборки, в который включён ваш комплект тестов. Следующим шагом будет его настройка.
Чтобы код тестов имел доступ к классам игры, мы создадим сборку кода классов и зададим ссылку в сборке Tests. Нажмите на папку Scripts, чтобы выбрать её. Нажмите правой клавишей на эту папку и выберите Create ▸ Assembly Definition.
Назовите файл GameAssembly.
Нажмите на папку Tests, а затем на файл определения сборки Tests. В Inspector нажмите на кнопку плюс под заголовком Assembly Definition References.
Вы увидите поле Missing Reference. Нажмите на точку рядом с этим полем, чтобы открыть окно выбора. Выберите файл GameAssembly.
Вы должны увидеть файл сборки GameAssembly в разделе ссылок. Нажмите на кнопку Apply, чтобы сохранить эти изменения.
Если вы не выполните эти действия, то не сможете ссылаться на файлы классов игры внутри файлов юнит-тестов. Разобравшись с этим, можно приступать к коду.
Дважды нажмите на скрипт 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<GameObject>("Prefabs/Game"));
game = gameGameObject.GetComponent<Game>();
// 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);
}
Здесь всего несколько строк кода, но они выполняют множество действий. Так что давайте остановимся и разберёмся с каждой частью:
SpawnAsteroid
возвращает экземпляр созданного астероида. Компонент Asteroid имеет метод Move
(если вам любопытно, как работает движение, то можете взглянуть на скрипт Asteroid внутри RW / Scripts).
Отлично, вы написали свой первый юнит-тест, но как узнать, что он работает? Разумеется, с помощью Test Runner! В окне Test Runner раскройте все строки со стрелками. Вы должны увидеть тест AsteroidsMoveDown
в списке с серыми кружками:
Серый кружок означает, что тест пока не выполнялся. Если тест был запущен и пройден, то рядом показывается зелёная стрелка. Если тест завершился с ошибкой, то рядом с ним будет отображён красный X. Запустим тест, нажав на кнопку RunAll.
При этом создастся временная сцена и будет запущен тест. После завершения вы должны увидеть, что тест пройден.
Вы успешно написали первый юнит-тест, утверждающий, что создаваемые астероиды движутся вниз.
Примечание: прежде чем начать писать собственные юнит-тесты, вам нужно понять реализацию, которую вы тестируете. Если вам любопытно, как работает тестируемая вами логика, то изучите код в папке RW / Scripts.
Прежде чем двигаться глубже в кроличью нору юнит-тестов, самое время рассказать, что такое интеграционные тесты, и чем они отличаются от юнит-тестирования.
Интеграционные тесты — это тесты, проверяющие как работают «модули» кода совместно. «Модуль» — это ещё один нечёткий термин. Важное отличие заключается в том, что интеграционные тесты должны тестировать работу ПО в настоящем продакшене (т.е. когда игрок по-настоящему играет в игру).
Допустим, вы сделали игру с боями, где игрок убивает монстров. Можно создать интеграционный тест, чтобы убедиться, что когда игрок убивает 100 врагов, открывается достижение («ачивка»).
Этот тест затронет несколько модулей кода. Скорее всего, он будет касаться физического движка (распознавание коллизий), диспетчеров врагов (отслеживающих здоровье врага и обрабатывающих урон, а также переходящих к другим связанным событиям) и трекера событий, отслеживающего все сработавшие события (например «монстр убит»). Затем, когда настанет время разблокировки достижения, он может вызвать диспетчер достижений.
Интеграционный тест будет симулировать игрока, убивающего 100 монстров, и проверять, разблокируется ли достижение. Он очень отличается от юнит-теста, потому что тестирует крупные компоненты кода, работающие совместно.
В этом туториале мы не будем изучать интеграционные тесты, но это должно показать разницу между «юнитом» работы (и то, зачем её юнит-тестируют) и «модулем» кода (и то, зачем его тестируют интеграционно).
Следующий тест будет тестировать конец игры, когда корабль сталкивается с астероидом. Открыв в редакторе кода TestSuite, добавьте под первым юнит-тестом показанный ниже тест и сохраните файл:
[UnityTest]
public IEnumerator GameOverOccursOnAsteroidCollision()
{
GameObject gameGameObject =
MonoBehaviour.Instantiate(Resources.Load<GameObject>("Prefabs/Game"));
Game game = gameGameObject.GetComponent<Game>();
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);
}
Мы уже видели бОльшую часть этого кода в предыдущем тесте, но здесь есть некоторые отличия:
gameOver
в скрипте Game принимает значение true. Флаг принимает значение true во время работы игры, когда уничтожается корабль, то есть мы тестируем, чтобы убедиться, что ему присваивается значение true после уничтожения корабля.Вернитесь в окно Test Runner и вы увидите, что там появился новый юнит-тест.
На этот раз мы запустим вместо всего комплекта тестов только этот. Нажмите на GameOverOccursOnAsteroidCollision, а затем на кнопку Run Selected.
И вуаля, мы прошли ещё один тест.
Вы могли заметить, что в двух наших тестах есть повторяющийся код: там, где создаётся игровой объект Game и где задаётся ссылка на скрипт Game:
GameObject gameGameObject = MonoBehaviour.Instantiate(Resources.Load<GameObject>("Prefabs/Game"));
game = gameGameObject.GetComponent<Game>();
Также вы заметите, что повтор есть в уничтожении игрового объекта 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<GameObject>("Prefabs/Game"));
game = gameGameObject.GetComponent<Game>();
}
Атрибут 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<GameObject>("Prefabs/Game"));
game = gameGameObject.GetComponent<Game>();
}
[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);
}
}
Подготовив упрощающие нашу жизнь методы настройки и разрушения, можно приступить к добавлению новых тестов, в которых они используются. Следующий тест должен проверять, что когда игрок нажимает New Game, значение gameOver bool не равно true. Добавьте такой тест в конец файла и сохраните его:
[UnityTest]
public IEnumerator NewGameRestartsGame()
{
//1
game.isGameOver = true;
game.NewGame();
//2
Assert.False(game.isGameOver);
yield return null;
}
Это уже должно казаться вам привычным, но стоит упомянуть следующее:
gameOver
должен иметь значение true. При вызове метода NewGame
он должен снова присваивать флагу значение false
.isGameOver
равен false
, что должно быть справедливо при вызове новой игры.Вернитесь в Test Runner и вы должны увидеть что там появился новый тест NewGameRestartsGame. Запустите этот тест, как мы делали это ранее, и вы увидите, что он успешно выполняется:
Следующим тестом нужно добавить тест того, что выстреливаемый кораблём лазерный луч летит вверх (аналогично первому написанному нами юнит-тесту). Откройте в редакторе файл 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);
}
Вот что делает этот код:
AsteroidsMoveDown
, только теперь мы утверждаем, что значение больше (то есть лазер движется вверх).Сохраните файл и вернитесь в Test Runner. Запустите тест LaserMovesUp и понаблюдайте за его прохождением:
Теперь вы уже должны начать разбираться, как всё устроено, поэтому настало время добавить последние два теста и завершить туториал.
Далее мы убедимся, что при попадании лазер уничтожает астероид. Откройте редактор и добавьте в конец 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);
}
Вот как это работает:
Assert.IsNull()
не будет работать в проверках Unity на null. При проверках на null в Unity, нужно явным образом использовать UnityEngine.Assertions.Assert, а не Assert из NUnit.Вернитесь в Test Runner и запустите новый тест. Вы увидите радующий нас зелёный значок.
Решение придерживаться юнит-тестов — непростое решение, и к нему не стоит относиться легкомысленно. Однако преимущества тестов стоят вложенных усилий. Существует даже методология разработки, называющаяся разработкой через тестирование (Test Driven Development, TDD).
Работая в рамках TDD, вы пишете тесты до написания самой логики приложения. Сначала вы создаёте тесты, убеждаетесь, что программа их не проходит, а затем пишете только код, предназначенный для прохождения тестов. Пусть это очень отличающийся подход к кодингу, но он гарантирует, что вы пишете код пригодным для тестирования образом.
Помните об этом, когда приступите к работе над следующим проектом. Но пока время писать собственные юнит-тесты, для которых нужна игра, которую мы вам и предоставили.
Примечание: нужно принять решение — тестировать только общие методы, или ещё и частные. Некоторые люди считают, что частные методы должны тестироваться только через использующие их общие методы. Это может сделать «юнит» кода, который нужно протестировать, довольно большим и может оказаться нежелательным. С другой стороны, тестирование частных методов может быть проблематичным и требовать особых фреймворков или инструментов рефлексии. Каждый из вариантов имеет свою плюсы и минусы, рассмотрение которых не входит в рамки данного туториала. В этом туториале все методы сделаны общими, чтобы их проще было отслеживать, поэтому не берите его в качестве примера при написании кода для продакшена.
Тестирование может быть большим вложением усилий, поэтому стоит рассмотреть достоинства и недостатки добавления юнит-тестирования в ваш проект:
У юнит-тестирования есть множество важных плюсов, в том числе и такие:
Однако у вас может и не быть времени или бюджета на юнит-тестирование. Вот его недостатки которые нужно учесть:
Настало время писать последний тест. Откройте редактор кода, добавьте показанный ниже код в конец файла 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);
}
Это важный тест, проверяющий, что когда игрок уничтожает астероид, счёт увеличивается. Вот из чего он состоит:
Сохраните код и вернитесь в Test Runner, чтобы запустить этот последний тест и проверить, проходит ли его игра:
Потрясающе! Все тесты пройдены.
В статье мы рассмотрели большой объём информации. Если вы хотите сравнить свою работу с финальным проектом, то посмотрите его в архиве [2], ссылка на который также указана в начале статьи.
Из этого туториала вы узнали, что такое юнит-тесты и как писать их в Unity. Кроме того, вы написали шесть юнит-тестов, которые успешно прошёл код, и познакомились с некоторыми из плюсов и минусов юнит-тестирования.
Чувствуете себя уверенно? Тогда можно написать ещё множество тестов. Изучите файлы классов игры и попробуйте написать юнит-тесты для других частей кода. Подумайте над добавлением тестов для следующих сценариев:
Если вы хотите повысить уровень своих знаний о юнит-тестировании, то стоит изучить внедрение зависимостей [4] и фреймворки для работы с mock-объектами [5]. Это может сильно упростить настройку тестов.
Также прочитайте документацию NUnit [6], чтобы узнать больше о фреймворке NUnit.
И не стесняйтесь делиться своими мыслями и вопросами на форумах.
Успешного тестирования!
Автор: PatientZero
Источник [7]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/modul-noe-testirovanie/320927
Ссылки в тексте:
[1] изучите сначала другие туториалы по этому движку: https://www.raywenderlich.com/unity
[2] отсюда: https://koenig-media.raywenderlich.com/uploads/2019/01/Crashteroids-project-files.zip
[3] wiki по NUnit: https://github.com/nunit/docs/wiki
[4] внедрение зависимостей: https://en.wikipedia.org/wiki/Dependency_injection
[5] фреймворки для работы с mock-объектами: https://en.wikipedia.org/wiki/Mock_object
[6] документацию NUnit: https://github.com/nunit/docs/wiki/NUnit-Documentation
[7] Источник: https://habr.com/ru/post/456090/?utm_source=habrahabr&utm_medium=rss&utm_campaign=456090
Нажмите здесь для печати.