Files

414 lines
13 KiB
C#
Raw Permalink Normal View History

2026-06-02 18:57:47 -04:00
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
2026-06-02 18:57:47 -04:00
{
[ExcludeFromCodeCoverage, ExcludeFromCoverage]
public class TestBase
{
// private readonly IDictionary<Object, SerializedObject> globalPropertyCache;
private readonly IDictionary<Object, SerializedObject> objectPropertyCache;
private readonly Queue<Object> destroyOnTestEnd;
private readonly Stack<Scene> closeOnTestEnd;
private readonly Queue<IDisposable> disposeOnTestEnd;
private readonly Queue<Object> destroyOnOneTimeEnd;
private readonly Stack<Scene> closeOnOneTimeEnd;
private readonly Queue<IDisposable> disposeOnOneTimeEnd;
[ExcludeFromCoverage]
protected TestBase()
{
// globalPropertyCache = new Dictionary<Object, SerializedObject>();
objectPropertyCache = new Dictionary<Object, SerializedObject>();
destroyOnTestEnd = new Queue<Object>();
closeOnTestEnd = new Stack<Scene>();
disposeOnTestEnd = new Queue<IDisposable>();
destroyOnOneTimeEnd = new Queue<Object>();
closeOnOneTimeEnd = new Stack<Scene>();
disposeOnOneTimeEnd = new Queue<IDisposable>();
}
/// <summary>
/// Called as part of NUnit framework. Override <see cref="OneTimeSetUp"/> instead.
/// </summary>
[OneTimeSetUp, ExcludeFromCoverage]
public void DoOneTimeSetUp()
{
OneTimeSetUp();
}
[UnityOneTimeSetUp, ExcludeFromCoverage]
public IEnumerator DoUnityOneTimeSetUp()
{
yield return UnityOneTimeSetUp();
}
/// <summary>
/// Called as part of NUnit framework. Override <see cref="SetUp"/> instead.
/// </summary>
[SetUp, ExcludeFromCoverage]
public void DoSetUp()
{
SetUp();
}
[UnitySetUp, ExcludeFromCoverage]
public IEnumerator DoUnitySetUp()
{
yield return UnitySetUp();
}
/// <summary>
/// Called as part of NUnit framework. Override <see cref="TearDown"/> instead.
/// </summary>
[TearDown, ExcludeFromCoverage]
public void DoTearDown()
{
TearDown();
objectPropertyCache.Clear();
}
[UnityTearDown, ExcludeFromCoverage]
public IEnumerator DoUnityTearDown()
{
yield return UnityTearDown();
Reset();
while (closeOnTestEnd.Count > 0)
yield return CloseLatestScene();
}
/// <summary>
/// Called as part of NUnit framework. Override <see cref="OneTimeTearDown"/> instead.
/// </summary>
[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);
}
/// <summary>
/// Pass a Unity object that will be destroyed at the end of the test.
/// </summary>
protected T RegisterTempTestObject<T>(T obj) where T : Object
{
destroyOnTestEnd.Enqueue(obj);
return obj;
}
/// <summary>
/// Pass a function that generates or returns a Unity object that will be destroyed at the end of the test.
/// </summary>
protected T RegisterTempTestObject<T>(Func<T> generator) where T : Object
{
var obj = generator();
return RegisterTempTestObject(obj);
}
/// <summary>
/// Pass a Unity object that will be destroyed at the end of all the tests in a given suite.
/// </summary>
protected T RegisterOneTimeTestObject<T>(T obj) where T : Object
{
destroyOnOneTimeEnd.Enqueue(obj);
return obj;
}
/// <summary>
/// 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.
/// </summary>
protected T RegisterOneTimeTestObject<T>(Func<T> generator) where T : Object
{
var obj = generator();
return RegisterOneTimeTestObject(obj);
}
/// <summary>
/// Pass a disposable object that will be cleaned up at the end of the test.
/// </summary>
protected T RegisterDisposableTempTestObject<T>(T obj) where T : IDisposable
{
disposeOnTestEnd.Enqueue(obj);
return obj;
}
/// <summary>
/// Pass a function that generates or returns a disposable object that will be cleaned up at the end of the test.
/// </summary>
/// <param name="generator"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
protected T RegisterDisposableTempTestObject<T>(Func<T> generator) where T : IDisposable
{
var obj = generator();
return RegisterDisposableTempTestObject(obj);
}
/// <summary>
/// Pass a disposable object that will be cleaned up at the end of the test.
/// </summary>
protected T RegisterDisposableOneTimeTestObject<T>(T obj) where T : IDisposable
{
disposeOnOneTimeEnd.Enqueue(obj);
return obj;
}
/// <summary>
/// Pass a function that generates or returns a disposable object that will be cleaned up at the end of the test.
/// </summary>
/// <param name="generator"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
protected T RegisterDisposableOneTimeTestObject<T>(Func<T> 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;
}
/// <summary>
/// Set a private value member of an object via C# reflection.
/// </summary>
/// <example><code>
/// public class SomeObject {
/// private int item;
/// }
///
/// var obj = new SomeObject();
/// SetReflectedValue(obj, "item", 25);
/// </code></example>
protected internal void SetReflectedValue(object targetObject, string targetProperty, object targetValue)
2026-06-02 18:57:47 -04:00
{
SetReflectedValue(targetObject, targetObject.GetType(), targetProperty, targetValue);
}
protected internal void SetReflectedValues(object targetObject, params ValueTuple<string, object>[] properties)
2026-06-02 18:57:47 -04:00
{
var type = targetObject.GetType();
for (var i = 0; i < properties.Length; i++)
SetReflectedValue(targetObject, type, properties[i].Item1, properties[i].Item2);
}
/// <summary>
/// Set a non-interface reference member of a MonoBehaviour via Unity reflection.
/// Will be applied depending on context of Test setup being used.
/// </summary>
/// <example><code>
/// 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);
/// </code></example>
protected internal void SetObjectReference(Object targetObj, string targetValue, Object value)
2026-06-02 18:57:47 -04:00
{
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();
}
/// <summary>
/// Set a non-interface reference member of a MonoBehaviour via Unity reflection.
/// Will be applied depending on context of Test setup being used.
/// </summary>
/// <example><code>
/// public class SomeObject : MonoBehaviour {
/// private SomeOtherObject item1;
/// private SomeAdditionalObject item2;
/// }
///
/// var gameObj = new GameObject("SomeObj");
/// var c = gameObj.AddComponent(typeof(SomeObject));
///
/// SetObjectReference(c, a);
/// </code></example>
protected internal void SetObjectReferences(Object targetObj, params ValueTuple<string, Object>[] properties)
2026-06-02 18:57:47 -04:00
{
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<Object> unityObjects)
{
if (unityObjects.Count == 0)
return;
while (unityObjects.Count != 0)
{
var temp = unityObjects.Dequeue();
if (temp != null)
Object.DestroyImmediate(temp);
}
}
private void CleanupDisposableObjects(Queue<IDisposable> 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<Object, SerializedObject> cache)
// {
// if (cache.Count == 0)
// return;
//
// foreach (var pair in cache)
// pair.Value.ApplyModifiedProperties();
// }
}
}