Концепция ECS очень хорошо подходит для модульного тестирования, поскольку вы можете буквально «создать экземпляр логики», передать какие-то данные в мир и заставить его работать. Но это когда уже работает. На этом пути есть проблемы.

В модульном тесте нет активного мира по умолчанию.

World.Active будет исключением нулевой ссылки. Мне нужно создать подкласс, чтобы позволить [SetUp] и [TearDown] пройти все тесты.

Обратите внимание, что [TearDown] и dispose на [SetUp] необходимы, потому что при одновременном запуске нескольких тестов Unity ничего не сбрасывает. Это включает в себя миры и сущности, в которых они находятся.

Наследование от ECSTestsFixtures

Unity предоставляет класс, который дает вам хорошую настройку и демонтаж с тестовым миром. Он также содержит свойства protected, чтобы вы могли получить EntityManager или World.

Пустая система

Это читерская система Unity, представленная в ECSTestsFixtures. (С protected свойством EmptySystem вы можете использовать его, чтобы получить его) Предполагаемая цель - это просто система, которая может «выкопать» группу компонентов из мира, чтобы вы могли получить данные для утверждения. (Не такой уж и «пустой» функционально!)

В вашем мире, созданном вручную, отсутствуют возможности гибридных инъекций.

Если вы касаетесь какой-либо системы с гибридным впрыском (ввод монокомпонента, трансформация и т. д.), тест не пройден.

Представьте модульное тестирование системы A, в которой объявлено системное внедрение.

[Inject] SystemB systemB;

Но этот метод модульного тестирования не затронет systemB.

Подход к тестированию заключается в создании мира и создании только этой системы A изолированно, затем мы можем попытаться вызвать ее методы или принудительно обновить после помещения некоторых сущностей в мир. (точно так же, как это делают тестовые скрипты в пакете ECS)

В системе B он имеет[Inject] MyData myData;

MyData содержит ComponentArray<T> (не Component*Data*Array), для работы которого требуется специальный хук для инъекций.

При входе в модульный тест, создании мира и GetOrCreateManager<SystemA> я получаю:

Код (CSharp):

[Inject] may only be used on ComponentDataArray<>, ComponentArray<>, TransformAccessArray, EntityArray, and int Length.

Это связано с тем, что GetOrCreateManager<SystemA> будет GetOrCreateManager<SystemB> по зависимости, а затем он столкнется с внедрением ComponentArray, для которого требуется хук, доступный только во время выполнения, чтобы не вызывать ошибку. То есть это может сделать только DefaultWorldInitialization.cs :

Решения, которые не работают:

  1. Я не могу отменить объявление SystemB только во время модульного тестирования, ни один препроцессор не может определить, выполняю ли я модульное тестирование или в реальном игровом процессе. Я не могу использовать Application.isPlaying, потому что он не входит в область кода. Платформа ECS сканирует в соответствии с атрибутом [Inject]. Таким образом, неизбежно тестировать SystemA изолированно, не перетаскивая SystemB вместе с ней, если я использовал системную инъекцию. код модульного теста не предназначен для использования этих инъекций. (Очевидно, что эти инжекты предназначены для тестирования использования/интеграции в игровом режиме, поскольку они связаны с игровыми объектами и компонентами)
    * В более широком смысле нельзя проводить модульное тестирование любой системы, которая имеет системную инъекцию в систему, содержащую этот пользовательский хук. -включенные инъекции.
  2. Попробуйте включить хуки по умолчанию в моем тестовом мире: недавно созданный мир модульного тестирования не может использовать ни один из встроенных хуков, все они «запечатаны приватно».
  3. Следующая идея состоит в том, чтобы создать новый InjectionHook, в котором `FieldTypeOfInterest` перехватывает ComponentArray+TransformAccessArray+GameObjectArray, а затем обнуляет все эти запрещенные поля. Я обнаружил, что хук требует [CustomInjectionHook], тогда CustomInjectionHookAttribute является «внутренним запечатанным».

Решение: отражение

Пробивайтесь через надоедливые internal с отражениями! Грязно, но работает. Ваш мир юнит-тестов теперь полностью функционален. Не забывайте часто проверять DefaultWorldInitialization.cs, если команда Unity добавила больше внутренних хуков.

Мой модульный тест ECS каждый раз терпит неудачу по разным причинам!

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

Дело в том, что я поместил struct с некоторым собственным распределением в массив static (опять же, зло). При первом запуске теста он терпит неудачу, но после завершения теста статическая переменная не очищается. Тогда в следующий раз, когда моя логика «утилизации» подумает, что внутри все еще что-то есть, но родной памяти внутри уже нет.

Я перезагрузил мир между тестами. Я думаю, что это несколько освобождает внутреннее собственное пространство массива.

У меня есть переменная initialized, которая будет истинной, если я создам экземпляр этого struct с помощью своего пользовательского конструктора (поскольку это не класс, структура по умолчанию может вызвать проблемы везде, и я не могу иметь собственный конструктор по умолчанию для struct), чтобы проверить это, но это не застрахован от проблемы static.

Последний пластырь .IsCreated тоже ничем не помогает, так как это не .IsExist . ( .IsCreated здесь все true , даже если базовая память не подлежит удалению, потому что она «была создана» раньше) Итак, в конце концов, между тестами я должен сделать свой собственный статический очиститель.

Чтобы увидеть проблему с static , все они сбрасываются при перекомпиляции скриптов, поэтому вы можете увидеть, вернется ли что-то в работу только один раз после перекомпиляции. Лучше не использовать static, потому что они делают ваши тесты хрупкими.

NUnit терпит неудачу bool1 и половина

Если вы используете Is.False вместо bool, вы получите глупо звучащее сообщение об ошибке:

Expected : False
But was :  False

(По непонятной причине есть еще один дополнительный пробел после двоеточия, но это может быть ключом к тому, что «False» на самом деле не «False», лол)

Поэтому вам нужно привести к (bool) только для модульного теста.

Сравнение float с half также не удается, говоря, что что-то вроде 7 не равно Unity.Mathematics.half, снова вы приводите к (float), и тест должен пройти.

World.CreateManager также создает всю связанную систему «[Inject]».

В тесте я создаю только SystemA, но SystemB полностью используется как в коде A, так и вне его GetExistingManager. Система C также доступна извне в соответствии с правилом внедрения системы, но если я удалю этот [Inject] SystemC и выполню GetExistingManager‹SystemC› -> .Update, это будет исключение нулевой ссылки.

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

Помните, что BarrierSystem должна быть «внедрена системой» для использования, это означает, что CreateManager также даст вам барьер, и если вы CreateManager вручную введете барьерную систему, это будет ошибкой. (Просто используйте GetOrCreateManager, если не уверены, какая система вызывает создание барьера или нет)

World.Active враг модульного тестирования!

Если возможно, не используйте World.Active, потому что при тестировании вы будете создавать свой тестовый мир. Вы можете сказать, что можете сделать его активным, но какой-то путь в вашем коде может снова создать мир и делать что-то в этом мире. В этот момент активный мир во вторично сотворенном мире будет неправильным. Вы могли бы сказать: делайте мир активным каждый раз, когда мы создаем новый мир. Но теперь у вас возникла проблема с необходимостью восстановить предыдущий активный мир после того, как этот новый мир будет удален… и так далее.

Чтобы сохранить «волшебное» свойство системы — это совокупность логики, которую можно запускать отдельно или вместе как систему, нам лучше не спрашивать об ее мире. Пусть работает в любом мире. Это делается с помощью таких вещей, как свойство EntityManager protected внутри области системного кода, которая получает EM своего мира. (правильные)

Проблемы с ручным обновлением

Это здорово с ECS, потому что, когда мы использовали MonoBehaviour, мы не могли «запустить кусок логики», как сейчас. Так что просто создайте тестовый мир, создайте несколько систем, которые вас интересуют, и последовательно запустите .Update, затем проверьте результат по данным ECS или в каком состоянии находится система сейчас. Чисто и красиво правда?

Не забывайте, что вы должны обновить барьеры

При обычном использовании барьер кажется почти автоматическим, как будто он должен работать после работы системы. Но в модульном тесте вам придется .Update и это делать!

Не создавайте преград для своего мира

И как получить барьер? На самом деле вам не нужно добавлять барьер в мир, поэтому просто используйте .GetExistingManager после того, как вы убедитесь, что у вас есть .CreateManager система, которая вводит этот барьер. Он будет создан вместе с системой в соответствии с правилом внедрения системы. (Барьер - это система)

Но если вы по ошибке используете .CreateManager для создания нового барьера для мира, вы в конечном итоге получите .Update барьер, который не имеет никакого отношения к реальному барьеру, используемому вашей системой. (Поэтому .GetOrCreateManager здесь безопаснее, но не используйте его слишком часто, так как .GetExistingManager имеет то преимущество, что может выдать ошибку из-за вашей ошибки)

[UpdateBefore] [UpdateAfter] теперь ничего не значит

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

EntityManager.CompleteAllJobs

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

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

Тестирование гибридной ECS

Если вы new GameObject() и AddComponent<YourComponentDataWrapper>() , это работает!

  • GameObjectEntity добавлено автоматически из-за[RequireComponent]
  • Также вызывается OnEnable, в результате чего появляется Entity.
  • Уничтожение также вызывает OnDisable и удаляет Entity
  • SetActive(false/true) приведет к тому, что Entity будет уничтожен или появится снова.

Короче говоря, это вполне можно проверить. Но одно: если вы запускаете тесты последовательно и оставляете созданные игровые объекты позади, они перейдут к следующему тесту! Это может привести к тому, что Entity перейдет к следующему делу, если вы оставите GameObjectEntity позади.

Чтобы исправить это, добавьте это [TearDown]

[TearDown]
public void CleanGO() 
{
    foreach (var go in SceneManager.GetSceneByName("").GetRootGameObjects())
    {
        GameObject.DestroyImmediate(go);
    }
}

Сцена «Без названия», которая немного мелькала при запуске модульных тестов, на самом деле называется "" вот так.

Тестирование гибридной ECS: создание экземпляра версии

Что, если у вас есть GameObject в реальном коде, ожидающем GameObject.Instantiate , а они содержат GameObjectEntity и друзей? Что бы GameObject подключался в инспекторе к какому-то префабу в проекте.

В модульном тесте у вас нет такого места для подключения, так как же «сделать» этот GameObject для теста?

В модульном тесте вы можете new GameObject использовать типы GOE, обертки и т. д. НЕМЕДЛЕННО в этой строке GOE уже делает свой OnEnable , поэтому, даже если вы намеревались, чтобы этот new GameObject был префабом для клонирования, вы уже получили Entity в своем тестовом мире.

Чтобы бороться с этим, после GameObject.Instantiate из этого сборного объекта обязательно SetActive(false) исходный объект. GOE активирует свой OnDisable и уничтожит свой Entity .

Обратите внимание, что GameObject.Instantiate также копирует активный статус, если вы думаете о SetActive(false) сборных объектах ДО создания экземпляров всех ваших экземпляров, они станут неактивными. (И вы не получаете никаких объектов, поскольку GOE еще не включен). Поэтому, если вы пойдете по этому пути, не забудьте активировать эти созданные объекты.