Skip to content

Commit

Permalink
Basic configuration tests (#909)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
Ryan089 authored Aug 20, 2022
1 parent 324de69 commit 123dffc
Show file tree
Hide file tree
Showing 15 changed files with 300 additions and 9 deletions.
8 changes: 8 additions & 0 deletions Assets/Scripts/EditorTests.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 25 additions & 0 deletions Assets/Scripts/EditorTests/EditorTests.asmdef
Original file line number Diff line number Diff line change
@@ -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
}
7 changes: 7 additions & 0 deletions Assets/Scripts/EditorTests/EditorTests.asmdef.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

188 changes: 188 additions & 0 deletions Assets/Scripts/EditorTests/LobbyTests.cs
Original file line number Diff line number Diff line change
@@ -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
/// <summary>
/// The name and path of the Scene that this script is testing.
/// </summary>
private const string SCENE_PATH = "Assets/Scenes/Lobby.unity";

/// <summary>
/// All of the prefabs in the project.
/// </summary>
private GameObject[] allPrefabs;

/// <summary>
/// All of the MonoBehaviours in the current scene.
/// </summary>
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 <guids.Length; i++)
{
allPrefabs[i] = AssetDatabase.LoadAssetAtPath <GameObject>(AssetDatabase.GUIDToAssetPath(guids[i]));
}
}

private void LoadAllMonoBehaviours()
{
// Find all the Monobehaviours in the currently open scene
allMonoBehaviours = GameObject.FindObjectsOfType<MonoBehaviour>();
}
#endregion

#region Tests

/// <summary>
/// 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.
/// </summary>
[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());
}

/// <summary>
/// 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.
/// </summary>
[Test]
public void SpecifiedMonoBehavioursWithinSceneAreOnTheirMandatedLayers()
{

// ARRANGE
bool allRelevantMonoBehavioursAreOnTheRightLayer = true;
StringBuilder sb = new StringBuilder();

// ACT
allRelevantMonoBehavioursAreOnTheRightLayer = CheckMonoBehavioursForCorrectLayer(allMonoBehaviours, ref sb);

// ASSERT
Assert.IsTrue(allRelevantMonoBehavioursAreOnTheRightLayer, sb.ToString());
}

/// <summary>
/// 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.
/// </summary>
[Test]
public void SpecifiedMonoBehavioursWithinPrefabsAreOnTheirMandatedLayers()
{

// ARRANGE
bool allRelevantMonoBehavioursAreOnTheRightLayer = true;
StringBuilder sb = new StringBuilder();
MonoBehaviour[] prefabMonoBehaviours;

// ACT
foreach (GameObject prefab in allPrefabs)
{
prefabMonoBehaviours = prefab.GetComponentsInChildren<MonoBehaviour>();
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
}
11 changes: 11 additions & 0 deletions Assets/Scripts/EditorTests/LobbyTests.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions Assets/Scripts/SS3D/Core/NotNullAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System;

/// <summary>
/// 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.
/// </summary>
[AttributeUsage(AttributeTargets.Field)]
public class NotNullAttribute : Attribute
{

}
11 changes: 11 additions & 0 deletions Assets/Scripts/SS3D/Core/NotNullAttribute.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions Assets/Scripts/SS3D/Core/RequiredLayerAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System;

/// <summary>
/// When decorated with this attribute, a Component requires that its GameObject
/// is on the specified layer.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class RequiredLayerAttribute : Attribute
{
public readonly string layer;

public RequiredLayerAttribute(string layer)
{
this.layer = layer;
}
}
11 changes: 11 additions & 0 deletions Assets/Scripts/SS3D/Core/RequiredLayerAttribute.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions Assets/Scripts/SS3D/Core/Systems/Lobby/View/GenericTabView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ namespace SS3D.Core.Systems.Lobby.View
/// <summary>
/// Generic class to manage a simple tab/panel UI
/// </summary>
[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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace SS3D.Core.Systems.Lobby.View
/// </summary>
public class LobbyCountdownView : NetworkBehaviour
{
[SerializeField] private TMP_Text _roundCountdownText;
[SerializeField][NotNull] private TMP_Text _roundCountdownText;

private int _roundSeconds;
private RoundState _roundState;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace SS3D.Core.Systems.Lobby.View
/// </summary>
public sealed class LobbyTabsView : MonoBehaviour
{
[SerializeField] private GenericTabView[] _categoryUi;
[SerializeField][NotNull] private GenericTabView[] _categoryUi;

private void Start()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<PlayerUsernameView> _playerUsernames;
[SerializeField] [NotNull] private List<PlayerUsernameView> _playerUsernames;

// The username panel prefab
[SerializeField] private GameObject _uiPrefab;
[SerializeField] [NotNull] private GameObject _uiPrefab;

public override void OnStartClient()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ namespace SS3D.Core.Systems.Lobby.View
/// <summary>
/// Simple Username ui element controller
/// </summary>
[RequiredLayer("UI")]
public sealed class PlayerUsernameView : MonoBehaviour
{
[SerializeField] private TMP_Text _nameLabel;
[SerializeField][NotNull] private TMP_Text _nameLabel;

public string Name => _nameLabel.text;

Expand Down

0 comments on commit 123dffc

Please sign in to comment.