414 lines
13 KiB
C#
414 lines
13 KiB
C#
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.Tests
|
|
{
|
|
[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>
|
|
private protected void SetReflectedValue(object targetObject, string targetProperty, object targetValue)
|
|
{
|
|
SetReflectedValue(targetObject, targetObject.GetType(), targetProperty, targetValue);
|
|
}
|
|
|
|
private protected void SetReflectedValues(object targetObject, params ValueTuple<string, object>[] properties)
|
|
{
|
|
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>
|
|
private protected 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();
|
|
}
|
|
|
|
/// <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>
|
|
private protected void SetObjectReferences(Object targetObj, params ValueTuple<string, Object>[] 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<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();
|
|
// }
|
|
}
|
|
}
|