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;