using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using Moq; using NUnit.Framework; using UnityEditor; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.TestTools; using Object = UnityEngine.Object; namespace BracerLib.Utility.Testing { [ExcludeFromCodeCoverage, ExcludeFromCoverage] public class TestBase { // private readonly IDictionary globalPropertyCache; private readonly IDictionary objectPropertyCache; private readonly Queue destroyOnTestEnd; private readonly Stack closeOnTestEnd; private readonly Queue disposeOnTestEnd; private readonly Queue destroyOnOneTimeEnd; private readonly Stack closeOnOneTimeEnd; private readonly Queue disposeOnOneTimeEnd; [ExcludeFromCoverage] protected TestBase() { // globalPropertyCache = new Dictionary(); objectPropertyCache = new Dictionary(); destroyOnTestEnd = new Queue(); closeOnTestEnd = new Stack(); disposeOnTestEnd = new Queue(); destroyOnOneTimeEnd = new Queue(); closeOnOneTimeEnd = new Stack(); disposeOnOneTimeEnd = new Queue(); } /// /// Called as part of NUnit framework. Override instead. /// [OneTimeSetUp, ExcludeFromCoverage] public void DoOneTimeSetUp() { OneTimeSetUp(); } [UnityOneTimeSetUp, ExcludeFromCoverage] public IEnumerator DoUnityOneTimeSetUp() { yield return UnityOneTimeSetUp(); } /// /// Called as part of NUnit framework. Override instead. /// [SetUp, ExcludeFromCoverage] public void DoSetUp() { SetUp(); } [UnitySetUp, ExcludeFromCoverage] public IEnumerator DoUnitySetUp() { yield return UnitySetUp(); } /// /// Called as part of NUnit framework. Override instead. /// [TearDown, ExcludeFromCoverage] public void DoTearDown() { TearDown(); objectPropertyCache.Clear(); } [UnityTearDown, ExcludeFromCoverage] public IEnumerator DoUnityTearDown() { yield return UnityTearDown(); Reset(); while (closeOnTestEnd.Count > 0) yield return CloseLatestScene(); } /// /// Called as part of NUnit framework. Override instead. /// [OneTimeTearDown, ExcludeFromCoverage] public void DoOneTimeTearDown() { OneTimeTearDown(); } [UnityOneTimeTearDown, ExcludeFromCoverage] public IEnumerator DoUnityOneTimeTearDown() { yield return UnityOneTimeTearDown(); CleanupUnityObjects(destroyOnOneTimeEnd); CleanupDisposableObjects(disposeOnOneTimeEnd); while (closeOnOneTimeEnd.Count > 0) yield return CloseLatestScene(true); } /// /// Pass a Unity object that will be destroyed at the end of the test. /// protected T RegisterTempTestObject(T obj) where T : Object { destroyOnTestEnd.Enqueue(obj); return obj; } /// /// Pass a function that generates or returns a Unity object that will be destroyed at the end of the test. /// protected T RegisterTempTestObject(Func generator) where T : Object { var obj = generator(); return RegisterTempTestObject(obj); } /// /// Pass a Unity object that will be destroyed at the end of all the tests in a given suite. /// protected T RegisterOneTimeTestObject(T obj) where T : Object { destroyOnOneTimeEnd.Enqueue(obj); return obj; } /// /// Pass a function that generates or returns a Unity object that will be destroyed at the end of all the tests in a given suite. /// protected T RegisterOneTimeTestObject(Func generator) where T : Object { var obj = generator(); return RegisterOneTimeTestObject(obj); } /// /// Pass a disposable object that will be cleaned up at the end of the test. /// protected T RegisterDisposableTempTestObject(T obj) where T : IDisposable { disposeOnTestEnd.Enqueue(obj); return obj; } /// /// Pass a function that generates or returns a disposable object that will be cleaned up at the end of the test. /// /// /// /// protected T RegisterDisposableTempTestObject(Func generator) where T : IDisposable { var obj = generator(); return RegisterDisposableTempTestObject(obj); } /// /// Pass a disposable object that will be cleaned up at the end of the test. /// protected T RegisterDisposableOneTimeTestObject(T obj) where T : IDisposable { disposeOnOneTimeEnd.Enqueue(obj); return obj; } /// /// Pass a function that generates or returns a disposable object that will be cleaned up at the end of the test. /// /// /// /// protected T RegisterDisposableOneTimeTestObject(Func generator) where T : IDisposable { var obj = generator(); return RegisterDisposableOneTimeTestObject(obj); } protected IEnumerator OpenScene(string scenePath, bool isOneTime = false) { var asyncOp = SceneManager.LoadSceneAsync(scenePath, LoadSceneMode.Additive); asyncOp!.completed += SetLoadedSceneActive; yield return asyncOp; var loadedScene = SceneManager.GetSceneAt(SceneManager.loadedSceneCount - 1); if (!isOneTime) closeOnTestEnd.Push(loadedScene); else closeOnOneTimeEnd.Push(loadedScene); } protected IEnumerator CloseLatestScene(bool isOneTime = false) { var targetStack = !isOneTime ? closeOnTestEnd : closeOnOneTimeEnd; if (!targetStack.TryPop(out var targetScene)) yield break; var asyncOp = SceneManager.UnloadSceneAsync(targetScene); asyncOp!.completed += SetLoadedSceneActive; yield return asyncOp; } protected void SetLoadedSceneActive(AsyncOperation asyncOperation) { SceneManager.SetActiveScene(SceneManager.GetSceneAt(SceneManager.loadedSceneCount - 1)); } [ExcludeFromCoverage] protected virtual void OneTimeSetUp() { } [ExcludeFromCoverage] protected virtual IEnumerator UnityOneTimeSetUp() { yield return null; } protected virtual void SetUp() { } protected virtual IEnumerator UnitySetUp() { yield return null; } protected virtual void TearDown() { } protected virtual IEnumerator UnityTearDown() { yield return null; } [ExcludeFromCoverage] protected virtual void OneTimeTearDown() { } [ExcludeFromCoverage] protected virtual IEnumerator UnityOneTimeTearDown() { yield return null; } /// /// Set a private value member of an object via C# reflection. /// /// /// public class SomeObject { /// private int item; /// } /// /// var obj = new SomeObject(); /// SetReflectedValue(obj, "item", 25); /// protected internal void SetReflectedValue(object targetObject, string targetProperty, object targetValue) { SetReflectedValue(targetObject, targetObject.GetType(), targetProperty, targetValue); } protected internal void SetReflectedValues(object targetObject, params ValueTuple[] properties) { var type = targetObject.GetType(); for (var i = 0; i < properties.Length; i++) SetReflectedValue(targetObject, type, properties[i].Item1, properties[i].Item2); } /// /// Set a non-interface reference member of a MonoBehaviour via Unity reflection. /// Will be applied depending on context of Test setup being used. /// /// /// public class SomeOtherObject { } /// /// public class SomeObject : MonoBehaviour { /// private SomeOtherObject item; /// } /// /// var gameObj = new GameObject("SomeObj"); /// var c = gameObj.AddComponent(typeof(SomeObject)); /// var someOtherObj = new SomeOtherObj(); /// /// SetObjectReference(c, "item", someOtherObj); /// protected internal void SetObjectReference(Object targetObj, string targetValue, Object value) { if (!objectPropertyCache.TryGetValue(targetObj, out var serializedObject)) { serializedObject = new SerializedObject(targetObj); objectPropertyCache.Add(targetObj, serializedObject); } var property = serializedObject.FindProperty(targetValue); property.objectReferenceValue = value; serializedObject.ApplyModifiedProperties(); } /// /// Set a non-interface reference member of a MonoBehaviour via Unity reflection. /// Will be applied depending on context of Test setup being used. /// /// /// public class SomeObject : MonoBehaviour { /// private SomeOtherObject item1; /// private SomeAdditionalObject item2; /// } /// /// var gameObj = new GameObject("SomeObj"); /// var c = gameObj.AddComponent(typeof(SomeObject)); /// /// SetObjectReference(c, a); /// protected internal void SetObjectReferences(Object targetObj, params ValueTuple[] properties) { for (var i = 0; i < properties.Length; i++) SetObjectReference(targetObj, properties[i].Item1, properties[i].Item2); } private void SetReflectedValue(object targetObject, Type type, string targetProperty, object targetValue) { var fieldProperty = type.GetField(targetProperty, BindingFlags.NonPublic | BindingFlags.Instance); if (fieldProperty == null) { Debug.LogWarning($"There is no property {targetProperty} to the target object."); return; } fieldProperty.SetValue(targetObject, targetValue); } private void Reset() { ResetAllMocks(); CleanupDisposableObjects(disposeOnTestEnd); CleanupUnityObjects(destroyOnTestEnd); } private void CleanupUnityObjects(Queue unityObjects) { if (unityObjects.Count == 0) return; while (unityObjects.Count != 0) { var temp = unityObjects.Dequeue(); if (temp != null) Object.DestroyImmediate(temp); } } private void CleanupDisposableObjects(Queue disposableObjects) { if (disposableObjects.Count == 0) return; while (disposableObjects.Count != 0) { var temp = disposableObjects.Dequeue(); temp?.Dispose(); } } [ExcludeFromCoverage] private void ResetAllMocks() { var mocks = GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) .Where(f => f.FieldType == typeof(Mock)).Select(f => (Mock)f.GetValue(this)); foreach (var m in mocks) m.Reset(); } // [ExcludeFromCoverage] // private void Apply(bool isGlobal = false) // { // ApplyPropertyCache(objectPropertyCache); // } // // [ExcludeFromCoverage] // private void ApplyPropertyCache(IDictionary cache) // { // if (cache.Count == 0) // return; // // foreach (var pair in cache) // pair.Value.ApplyModifiedProperties(); // } } }