Pull to refresh

Тестирование компонентов в Unity Engine

Reading time5 min
Views25K
В продолжение статьи о КОП я подробней расскажу о тестировании компонентов.
Существует много информации о юнит-тестировании и BDD, эта статья будет посвящена настройке SpecFlow для работы с Unity Engine, а также содержать общие рекомендации по созданию тестируемой архитектуры игры с компонентно-ориентированным подходом.

Общие правила разработки компонента, который можно будет легко протестировать, я сформулировал так:
1) То, что может видеть/слышать только человек, должно быть в отдельном компоненте.
2) У компонента должно быть «дефолтное» поведение. Лучше считать его эталонным.
3) Не забывайте: один компонент — одно поведение.
Таким образом, я внёс корректировки в свою реализацию core gameplay игры жанра Tower Defense:
Некоторые компоненты обращаются к Renderer через GetComponent() —это ненужная зависимость от обязательного присутствия компонента Renderer. Вся визуализация должна быть в отдельных компонентах. Указал дефолтные значения всем полям: это упростит и саму настройку компонента. Также расширил функционал игры, введя Игрока:
Игрок — человек, чьё поведение состоит в передаче управления в игру, посредством устройств ввода.
Теперь, для начала игры, надо нажать кнопку «Начать игру» в UI, после чего выбрать в UI башни для постройки с указанием куда их построить. И когда все башни будут построены, начнётся игра.
Но вот вопрос: для каких компонентов нужны Unit тесты, а какие лучше проверять по спецификации?
Ответ на этот вопрос может быть разным в зависимости от количества компонентов и общей сложности проекта. Стоит помнить, что общее поведение игрового объекта складывается из поведений компонентов. И рекомендация у меня такая:
Необходимо оценить сложность реализации логики внутри компонента. Если в ней легко допустить ошибку, стоит подстраховаться и сделать отдельный юнит-тест для компонента. В других случаях можно сделать общую спецификацию, которая будет проверять поведение игрового объекта в целом.
Подготовительная работа.
Из дополнений к Unity, нам понадобится следующее:
1) UnityTestTools — runner NUnit-тестов от разработчиков Unity в среде редактора.
2) Visual Studio Tools (бывший UnityVS) — плагин для отладки.
Для Visual Studio понадобится SpecFlow
И начнём мы с Unit-тестов.
Многие добавляют юнит-тесты непосредственно в сам проект, но не стоит утяжелять сборку лишним. Не так много времени займёт выделение набора тестов в отдельную сборку, которая не будет включена в финальную версию игры (build).
После добавления UnityTestTools в Unity, нужно открыть решение (сгенерированное UnityVs) и добавить в него новый проект типа «библиотека классов», в ней и будут наши юнит-тесты компонентов. Для примера, назовём сборку TowerDefenseCore.UnitTests. Важным моментом является настройка сборки:
1) В references сборки надо добавить:
— «nunit.framework.dll» и «nunit.core.dll» из Assets\UnityTestTools\UnitTesting\Editor\NUnit\Libs
— «UnityEngine.dll» из Library\UnityAssemblies
— Сборку «Assembly-CSharp.dll» из решения
Важно: не надо их копировать локально.
2) Необходимо настроить свойства сборки, указав путь вывода. Сборка должна быть в директории Assets — например, Assets\Tests.
Написание юнит-тестов, исполняемых в Unity Test Runner, ничем не отличается от обычных юнит-тестов. Однако, есть несколько важных моментов.
Первое: делать инстанциирование игрового объекта (метод GameObject.Instantiate) не обязательно.
Вот простой тест компонента DamageApplicator:
[Test]
public void DamageApplicator_DirectDamage()
{
        var damageApplicator = new DamageApplicator();
        var hp = damageApplicator.DirectDamage(100, 10);
        Assert.AreEqual(90f, hp);
}

DamageApplicator не имеет никаких зависимостей от других компонентов, которые разрешались бы в методе Awake/Start/OnEnable. Но если компонент использует Awake с этой целью, то его нужно вызывать принудительно:
[Test]
public void Hittable_DeadOnDirectDamage()
{
        var componentsHolder = new GameObject();
	    //Добавляем компонент, от которого зависит тестируемый компонент
        componentsHolder.AddComponent<DamageAplicator>();
        var hittable = componentsHolder.AddComponent<Hittable>();
        hittable.Awake();//Важно: мы не сделали инстанс объекта, потому вручную внедрим зависимость от DamageApplicator
			
        hittable.DirectDamage(50);
        Assert.AreEqual(50, hittable.HP);
        hittable.DirectDamage(50);
        Assert.AreEqual(true, hittable.IsDead);
}

Второе: проверка работы компонентов, использующих Coroutine. Тут важно указывать MaxTime, рассчитанное эталонное время выполнения (если оно есть) или в цикле while дополнительную проверку, пройден ли тест.
[Test]
[MaxTime(10000)]//BeginDPS наносит 5 единиц урона каждый 0.5 сек, у цели 100HP
public void DamageInflictor_BeginDPS()
{
        var target = new GameObject();
        target.AddComponent<DamageApplicator>();
        var hittable = target.AddComponent<Hittable>();
        hittable.Awake();
        var tower = new GameObject();
        var dmger = tower.AddComponent<DamageInflictor>();
        dmger.BeginDPS(hittable);//Запускаем Coroutine        
        while (dmger.inflictDamage().MoveNext())//Симулируем работу Coroutine
                Thread.Sleep(100);
        Assert.True(hittable.IsDead);
}

Третий момент: лучше тестировать поведение каждого компонента, а не сцены в целом. В настройках Test Runner в опциях укажите Run test on a new scene.
Соберём сборку, переключимся на Unity и в меню Unity Test Tools выберем Test Runner. Как увидите, тесты из сборки появятся для запуска.

Настройка SpecFlow.
Создадим библиотеку классов TowerDefenseCore.Specs в решении. Настроим её:
1) Добавим в неё пакеты: Install-Package SpecFlow.NUnit
2) Добавим зависимости сборки:
— «TechTalk.SpecFlow.dll» из \packages\SpecFlow.1.9.0\lib\net35
— «UnityEngine.dll» из Library\UnityAssemblies
— Сборку «Assembly-CSharp.dll» из решения
Важно: не надо их копировать локально.
3) Необходимо настроить свойства сборки, указав путь вывода. Сборка должна быть в директории Assets — например Assets\Tests.
4) Важно: скопируем «TechTalk.SpecFlow.dll» в Assets\UnityTestTools\UnitTesting\Editor\NUnit\Libs.
Теперь в Test Runner помимо тестов из TowerDefenseCore.UnitTests будут и Steps из TowerDefenseCore.Specs.

Простой пример Feature, проверяющий поведение крипа при получении урона:
Feature: CreepLogic
Creep alive, take damage and dead.
@ creep
Scenario: Check creep is dead
Given Creep is alive
When Creep take damage 100
Then Creep is dead
И сгенерированные шаги:
[Binding]
public class CreepLogicSteps
{
     private Hittable _creep;
     [Given(@"Creep is alive")]
     public void GivenCreepIsAlive()
     {
         var componentsHolder = new GameObject();
         componentsHolder.AddComponent<DamageApplicator>();
         _creep = componentsHolder.AddComponent<Hittable>();
         _creep.Awake();
     }
     [When(@"Creep take damage (.*)")]
     public void WhenCreepTakeDamage(int dmg)
     {
         _creep.DirectDamage(dmg);
     }
     [Then(@"Creep is dead")]
     public void ThenCreepIsDead()
     {
         NUnitFramework.Assert.AreEqual(true, _creep.IsDead);
     }
}

В Test Runner тест будет отображён с названием CheckCreepIsDead.


В начале статьи я упоминал про поведение Игрока. Какие есть способы автоматизировать проверку его поведения без участия человека — тема для отдельной статьи.
Tags:
Hubs:
Total votes 11: ↑9 and ↓2+7
Comments1

Articles