From 123dffc8e499d2e6d3e9d08e39a92d8ec2fc5f48 Mon Sep 17 00:00:00 2001 From: Ryan089 <64360820+Ryan089@users.noreply.github.com> Date: Sun, 21 Aug 2022 03:57:59 +1000 Subject: [PATCH] Basic configuration tests (#909) * Basic configuration tests to: - Confirm required serialized variables have been initialized - Confirm game objects in a particular scene are on the correct layer - Confirm prefabs are on the correct layer * Substantially refactored configuration testing code. Specifying not-null fields and mandatory layers no longer occurs within DataSource.cs. Rather, attributes have been created to decorate those fields / classes directly, and these are accessed in the test through reflection. * Extracted duplicate code from mandated layer checks. * Editor Tests moved into Scripts folder. --- Assets/Scripts/EditorTests.meta | 8 + Assets/Scripts/EditorTests/EditorTests.asmdef | 25 +++ .../EditorTests/EditorTests.asmdef.meta | 7 + Assets/Scripts/EditorTests/LobbyTests.cs | 188 ++++++++++++++++++ Assets/Scripts/EditorTests/LobbyTests.cs.meta | 11 + Assets/Scripts/SS3D/Core/NotNullAttribute.cs | 12 ++ .../SS3D/Core/NotNullAttribute.cs.meta | 11 + .../SS3D/Core/RequiredLayerAttribute.cs | 16 ++ .../SS3D/Core/RequiredLayerAttribute.cs.meta | 11 + .../Core/Systems/Lobby/View/GenericTabView.cs | 5 +- .../Systems/Lobby/View/LobbyCountdownView.cs | 2 +- .../Core/Systems/Lobby/View/LobbyRoundView.cs | 2 +- .../Core/Systems/Lobby/View/LobbyTabsView.cs | 2 +- .../Lobby/View/PlayerUsernameListView.cs | 6 +- .../Systems/Lobby/View/PlayerUsernameView.cs | 3 +- 15 files changed, 300 insertions(+), 9 deletions(-) create mode 100644 Assets/Scripts/EditorTests.meta create mode 100644 Assets/Scripts/EditorTests/EditorTests.asmdef create mode 100644 Assets/Scripts/EditorTests/EditorTests.asmdef.meta create mode 100644 Assets/Scripts/EditorTests/LobbyTests.cs create mode 100644 Assets/Scripts/EditorTests/LobbyTests.cs.meta create mode 100644 Assets/Scripts/SS3D/Core/NotNullAttribute.cs create mode 100644 Assets/Scripts/SS3D/Core/NotNullAttribute.cs.meta create mode 100644 Assets/Scripts/SS3D/Core/RequiredLayerAttribute.cs create mode 100644 Assets/Scripts/SS3D/Core/RequiredLayerAttribute.cs.meta diff --git a/Assets/Scripts/EditorTests.meta b/Assets/Scripts/EditorTests.meta new file mode 100644 index 0000000000..9839046cae --- /dev/null +++ b/Assets/Scripts/EditorTests.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3e36804538508074b8fc6729483efb2e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/EditorTests/EditorTests.asmdef b/Assets/Scripts/EditorTests/EditorTests.asmdef new file mode 100644 index 0000000000..50ee1a62e3 --- /dev/null +++ b/Assets/Scripts/EditorTests/EditorTests.asmdef @@ -0,0 +1,25 @@ +{ + "name": "EditorTests", + "rootNamespace": "", + "references": [ + "UnityEngine.TestRunner", + "UnityEditor.TestRunner", + "SS3D.Core", + "FishNet.Runtime" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": true, + "precompiledReferences": [ + "nunit.framework.dll" + ], + "autoReferenced": false, + "defineConstraints": [ + "UNITY_INCLUDE_TESTS" + ], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Assets/Scripts/EditorTests/EditorTests.asmdef.meta b/Assets/Scripts/EditorTests/EditorTests.asmdef.meta new file mode 100644 index 0000000000..31f1890c98 --- /dev/null +++ b/Assets/Scripts/EditorTests/EditorTests.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: cc2236b6260a5e140bce7ffd8967d7e9 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/EditorTests/LobbyTests.cs b/Assets/Scripts/EditorTests/LobbyTests.cs new file mode 100644 index 0000000000..11b85c3645 --- /dev/null +++ b/Assets/Scripts/EditorTests/LobbyTests.cs @@ -0,0 +1,188 @@ +using NUnit.Framework; +using UnityEditor.SceneManagement; +using System.Reflection; +using UnityEngine; +using System; +using UnityEditor; +using System.Text; + +public class LobbyTests +{ + #region Class variables + /// + /// The name and path of the Scene that this script is testing. + /// + private const string SCENE_PATH = "Assets/Scenes/Lobby.unity"; + + /// + /// All of the prefabs in the project. + /// + private GameObject[] allPrefabs; + + /// + /// All of the MonoBehaviours in the current scene. + /// + private MonoBehaviour[] allMonoBehaviours; + + #endregion + + #region Test set up + [OneTimeSetUp] + public void SetUp() + { + EditorSceneManager.OpenScene(SCENE_PATH); + LoadAllPrefabs(); + LoadAllMonoBehaviours(); + } + + private void LoadAllPrefabs() + { + // Find all the prefabs in the project hierarchy (i.e. NOT in a scene) + string[] guids = AssetDatabase.FindAssets("t:prefab"); + + // Create our array of prefabs + allPrefabs = new GameObject[guids.Length]; + + // Populate the array + for (int i = 0; i (AssetDatabase.GUIDToAssetPath(guids[i])); + } + } + + private void LoadAllMonoBehaviours() + { + // Find all the Monobehaviours in the currently open scene + allMonoBehaviours = GameObject.FindObjectsOfType(); + } + #endregion + + #region Tests + + /// + /// Test to confirm that MonoBehaviours have serialized fields (marked by NotNullAttribute) initialized. + /// The purpose of this test is to prevent NullReferenceExceptions caused by failing to initialize MonoBehaviour fields. + /// + [Test] + public void SpecifiedFieldsWithinSceneAreNotNull() + { + + // ARRANGE + bool allRelevantFieldsHaveBeenSet = true; + BindingFlags flags = GetBindingFlags(); + StringBuilder sb = new StringBuilder(); + + // ACT - Check each MonoBehaviour in the scene + foreach (MonoBehaviour mono in allMonoBehaviours) + { + // Get all fields from the MonoBehaviour using reflection + Type monoType = mono.GetType(); + FieldInfo[] objectFields = monoType.GetFields(flags); + + // Check the fields to see if they have a NotNullAttribute + for (int i = 0; i < objectFields.Length; i++) + { + NotNullAttribute attribute = Attribute.GetCustomAttribute(objectFields[i], typeof(NotNullAttribute)) as NotNullAttribute; + if (attribute != null) + { + // Once we are here, we have found a MonoBehaviour field with a NotNullAttribute. + // We now need to test the field to see if it is a value set. + var fieldValue = objectFields[i].GetValue(mono); + + if (fieldValue == null || fieldValue.ToString() == "null") + { + // The test will fail, as the MonoBehaviour SHOULD have had some value in the required field, but DID NOT. + // We are delaying the assertion so that all errors are identified in the console, rather than requiring the + // test to be run multiple times (and only identifying a single breach each time). + allRelevantFieldsHaveBeenSet = false; + sb.Append($"-> Scene object '{mono.gameObject.name}' does not have {objectFields[i].Name} field set in {monoType.Name} script.\n"); + } + } + } + } + + // ASSERT + Assert.IsTrue(allRelevantFieldsHaveBeenSet, sb.ToString()); + } + + /// + /// Test to confirm that GameObjects within the tested scene are on the correct layers. + /// The purpose of this test is to ensure layer-based collisions, raycasts, rendering etc function correctly. + /// + [Test] + public void SpecifiedMonoBehavioursWithinSceneAreOnTheirMandatedLayers() + { + + // ARRANGE + bool allRelevantMonoBehavioursAreOnTheRightLayer = true; + StringBuilder sb = new StringBuilder(); + + // ACT + allRelevantMonoBehavioursAreOnTheRightLayer = CheckMonoBehavioursForCorrectLayer(allMonoBehaviours, ref sb); + + // ASSERT + Assert.IsTrue(allRelevantMonoBehavioursAreOnTheRightLayer, sb.ToString()); + } + + /// + /// Test to confirm that prefabs within the project are on the correct layers. + /// The purpose of this is to ensure that any prefabs instantiated at runtime are added to the correct layer. + /// + [Test] + public void SpecifiedMonoBehavioursWithinPrefabsAreOnTheirMandatedLayers() + { + + // ARRANGE + bool allRelevantMonoBehavioursAreOnTheRightLayer = true; + StringBuilder sb = new StringBuilder(); + MonoBehaviour[] prefabMonoBehaviours; + + // ACT + foreach (GameObject prefab in allPrefabs) + { + prefabMonoBehaviours = prefab.GetComponentsInChildren(); + allRelevantMonoBehavioursAreOnTheRightLayer = + allRelevantMonoBehavioursAreOnTheRightLayer && CheckMonoBehavioursForCorrectLayer(prefabMonoBehaviours, ref sb); + } + + // ASSERT + Assert.IsTrue(allRelevantMonoBehavioursAreOnTheRightLayer, sb.ToString()); + } + #endregion + + #region Helper functions + private BindingFlags GetBindingFlags() + { + BindingFlags flags = BindingFlags.Public | + BindingFlags.Instance | + BindingFlags.NonPublic; + return flags; + } + + private bool CheckMonoBehavioursForCorrectLayer(MonoBehaviour[] monos, ref StringBuilder sb) + { + bool allRelevantMonoBehavioursAreOnTheRightLayer = true; + foreach (MonoBehaviour mono in monos) + { + Type monoType = mono.GetType(); + RequiredLayerAttribute attribute = Attribute.GetCustomAttribute(monoType, typeof(RequiredLayerAttribute)) as RequiredLayerAttribute; + if (attribute != null) + { + // Once we are here, we have found a MonoBehaviour with a RequiredLayerAttribute. + // We now need to test the GameObject to see if it is on the layer that is mandated. + + if (mono.gameObject.layer != LayerMask.NameToLayer(attribute.layer)) + { + // The test will fail, as the GameObject SHOULD have had been on a specific layer, but WAS NOT. + // We are delaying the assertion so that all errors are identified in the console, rather than requiring the + // test to be run multiple times (and only identifying a single breach each time). + allRelevantMonoBehavioursAreOnTheRightLayer = false; + sb.Append($"-> {monoType.Name} script requires object '{mono.gameObject.name}' to be on {attribute.layer} layer, but it was on {LayerMask.LayerToName(mono.gameObject.layer)} layer.\n"); + } + } + } + return allRelevantMonoBehavioursAreOnTheRightLayer; + } + + #endregion +} diff --git a/Assets/Scripts/EditorTests/LobbyTests.cs.meta b/Assets/Scripts/EditorTests/LobbyTests.cs.meta new file mode 100644 index 0000000000..1ffec9ccd0 --- /dev/null +++ b/Assets/Scripts/EditorTests/LobbyTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5b91816711316584bbdaf4b97ff51b16 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/SS3D/Core/NotNullAttribute.cs b/Assets/Scripts/SS3D/Core/NotNullAttribute.cs new file mode 100644 index 0000000000..1922f2d77d --- /dev/null +++ b/Assets/Scripts/SS3D/Core/NotNullAttribute.cs @@ -0,0 +1,12 @@ +using System; + +/// +/// When decorated with this attribute, a field requires that it be set +/// to a non-null value prior to runtime. It should not be used on fields +/// that you intend to initialize at runtime. +/// +[AttributeUsage(AttributeTargets.Field)] +public class NotNullAttribute : Attribute +{ + +} diff --git a/Assets/Scripts/SS3D/Core/NotNullAttribute.cs.meta b/Assets/Scripts/SS3D/Core/NotNullAttribute.cs.meta new file mode 100644 index 0000000000..95a1e8b158 --- /dev/null +++ b/Assets/Scripts/SS3D/Core/NotNullAttribute.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 82606b2e10a1c9c499a42b2e2d4478b7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/SS3D/Core/RequiredLayerAttribute.cs b/Assets/Scripts/SS3D/Core/RequiredLayerAttribute.cs new file mode 100644 index 0000000000..4dfa85db65 --- /dev/null +++ b/Assets/Scripts/SS3D/Core/RequiredLayerAttribute.cs @@ -0,0 +1,16 @@ +using System; + +/// +/// When decorated with this attribute, a Component requires that its GameObject +/// is on the specified layer. +/// +[AttributeUsage(AttributeTargets.Class)] +public class RequiredLayerAttribute : Attribute +{ + public readonly string layer; + + public RequiredLayerAttribute(string layer) + { + this.layer = layer; + } +} diff --git a/Assets/Scripts/SS3D/Core/RequiredLayerAttribute.cs.meta b/Assets/Scripts/SS3D/Core/RequiredLayerAttribute.cs.meta new file mode 100644 index 0000000000..46bf80cdc0 --- /dev/null +++ b/Assets/Scripts/SS3D/Core/RequiredLayerAttribute.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 26f107b7a54bcec4c977b5d355b99e6c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/SS3D/Core/Systems/Lobby/View/GenericTabView.cs b/Assets/Scripts/SS3D/Core/Systems/Lobby/View/GenericTabView.cs index 105929ce69..3a7d4515ab 100644 --- a/Assets/Scripts/SS3D/Core/Systems/Lobby/View/GenericTabView.cs +++ b/Assets/Scripts/SS3D/Core/Systems/Lobby/View/GenericTabView.cs @@ -6,11 +6,12 @@ namespace SS3D.Core.Systems.Lobby.View /// /// Generic class to manage a simple tab/panel UI /// + [RequiredLayer("UI")] public sealed class GenericTabView : MonoBehaviour { [SerializeField] private bool _active; - [SerializeField] private Transform _panelUI; - [SerializeField] private Button _tabButton; + [SerializeField] [NotNull] private Transform _panelUI; + [SerializeField] [NotNull] private Button _tabButton; public Button Button => _tabButton; public bool Active => _active; diff --git a/Assets/Scripts/SS3D/Core/Systems/Lobby/View/LobbyCountdownView.cs b/Assets/Scripts/SS3D/Core/Systems/Lobby/View/LobbyCountdownView.cs index 6c5d4ca53e..bca8b712a6 100644 --- a/Assets/Scripts/SS3D/Core/Systems/Lobby/View/LobbyCountdownView.cs +++ b/Assets/Scripts/SS3D/Core/Systems/Lobby/View/LobbyCountdownView.cs @@ -12,7 +12,7 @@ namespace SS3D.Core.Systems.Lobby.View /// public class LobbyCountdownView : NetworkBehaviour { - [SerializeField] private TMP_Text _roundCountdownText; + [SerializeField][NotNull] private TMP_Text _roundCountdownText; private int _roundSeconds; private RoundState _roundState; diff --git a/Assets/Scripts/SS3D/Core/Systems/Lobby/View/LobbyRoundView.cs b/Assets/Scripts/SS3D/Core/Systems/Lobby/View/LobbyRoundView.cs index f89807fa19..d744a0a9d6 100644 --- a/Assets/Scripts/SS3D/Core/Systems/Lobby/View/LobbyRoundView.cs +++ b/Assets/Scripts/SS3D/Core/Systems/Lobby/View/LobbyRoundView.cs @@ -10,7 +10,7 @@ namespace SS3D.Core.Systems.Lobby.View { public class LobbyRoundView : NetworkBehaviour { - [SerializeField] private Button _embarkButton; + [SerializeField][NotNull] private Button _embarkButton; private void Start() { diff --git a/Assets/Scripts/SS3D/Core/Systems/Lobby/View/LobbyTabsView.cs b/Assets/Scripts/SS3D/Core/Systems/Lobby/View/LobbyTabsView.cs index aa91792807..1892229c3b 100644 --- a/Assets/Scripts/SS3D/Core/Systems/Lobby/View/LobbyTabsView.cs +++ b/Assets/Scripts/SS3D/Core/Systems/Lobby/View/LobbyTabsView.cs @@ -7,7 +7,7 @@ namespace SS3D.Core.Systems.Lobby.View /// public sealed class LobbyTabsView : MonoBehaviour { - [SerializeField] private GenericTabView[] _categoryUi; + [SerializeField][NotNull] private GenericTabView[] _categoryUi; private void Start() { diff --git a/Assets/Scripts/SS3D/Core/Systems/Lobby/View/PlayerUsernameListView.cs b/Assets/Scripts/SS3D/Core/Systems/Lobby/View/PlayerUsernameListView.cs index 1df60adf43..220ff42bba 100644 --- a/Assets/Scripts/SS3D/Core/Systems/Lobby/View/PlayerUsernameListView.cs +++ b/Assets/Scripts/SS3D/Core/Systems/Lobby/View/PlayerUsernameListView.cs @@ -15,13 +15,13 @@ namespace SS3D.Core.Systems.Lobby.View public sealed class PlayerUsernameListView : NetworkBehaviour { // The UI element this is linked to - [SerializeField] private Transform _root; + [SerializeField] [NotNull] private Transform _root; // Username list, local list that is "networked" by the SyncList on LobbyManager - [SerializeField] private List _playerUsernames; + [SerializeField] [NotNull] private List _playerUsernames; // The username panel prefab - [SerializeField] private GameObject _uiPrefab; + [SerializeField] [NotNull] private GameObject _uiPrefab; public override void OnStartClient() { diff --git a/Assets/Scripts/SS3D/Core/Systems/Lobby/View/PlayerUsernameView.cs b/Assets/Scripts/SS3D/Core/Systems/Lobby/View/PlayerUsernameView.cs index 16043a64bd..419e679a03 100644 --- a/Assets/Scripts/SS3D/Core/Systems/Lobby/View/PlayerUsernameView.cs +++ b/Assets/Scripts/SS3D/Core/Systems/Lobby/View/PlayerUsernameView.cs @@ -6,9 +6,10 @@ namespace SS3D.Core.Systems.Lobby.View /// /// Simple Username ui element controller /// + [RequiredLayer("UI")] public sealed class PlayerUsernameView : MonoBehaviour { - [SerializeField] private TMP_Text _nameLabel; + [SerializeField][NotNull] private TMP_Text _nameLabel; public string Name => _nameLabel.text;