Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

How to restructure tests which open and iterate over multiple scenes

Discussion in 'Testing & Automation' started by Xarbrough, Jan 29, 2020.

  1. Xarbrough

    Xarbrough

    Joined:
    Dec 11, 2014
    Posts:
    1,188
    I have a series of tests which I'd like to refactor because they contain a lot of duplicated code. The current structure is like so:

    Code (CSharp):
    1. public class SceneValidationTests
    2. {
    3.     [Test]
    4.     public void LevelScenes_OnStart_ContainObjectBla()
    5.     {
    6.         // Get list of all level scenes.
    7.         // Iterate and load all scenes.
    8.         // Assert GameObject is present.
    9.     }
    10.  
    11.     [Test]
    12.     public void LevelScenes_OnStart_ContainObjectBla2()
    13.     {
    14.         // Get list of all level scenes.
    15.         // Iterate and load all scenes.
    16.         // Assert GameObject 2 is present.
    17.     }
    18. }
    Every test verifies that a specific object must be present or some settings need to make sense in its context (kind of like sanity checks for level designers). Each tests needs to verify these conditions for all scenes, so they all start by opening the scenes, checking the condition and iterating through the list of scenes. This means, that for each test, all scenes are opened, which can become quite slow.

    It would be much better, if I could open every scene only once and then run all tests for this scene. Similar to this:

    Code (CSharp):
    1. public class SceneValidationTests
    2. {
    3.     [TestEntry] // This attribute does not exist, but it would be called as a parent to all other tests.
    4.     public void RunForAllScenes()
    5.     {
    6.         // Iterate and load all scenes
    7.         // Automatically call all test methods
    8.     }
    9.  
    10.     [Test]
    11.     public void LevelScenes_OnStart_ContainObjectBla(Scene scene)
    12.     {
    13.         // Assert GameObject is present.
    14.     }
    15.  
    16.     [Test]
    17.     public void LevelScenes_OnStart_ContainObjectBla2(Scene scene)
    18.     {
    19.         // Assert GameObject 2 is present.
    20.     }
    21. }
    What's a good way to structure tests in this manner? I would like to keep each test its own [Test]-case so it shows up as a single entry in the test runner and each test should be able to run on its own. So, ideally, I would like to define this kind of method similar to TestSetUp, which runs before a test or kind of wraps it, but also runs only once if multiple tests should be run.

    Is there an easy solution to this or would this be a new feature possible to implement in the future?
     
  2. zardini123

    zardini123

    Joined:
    Jan 5, 2013
    Posts:
    68
    A bit unrelated, but how are you going about testing multiple scenes? Have you figured out any specific code to load a scene in Play mode tests?
     
  3. Xarbrough

    Xarbrough

    Joined:
    Dec 11, 2014
    Posts:
    1,188
    I'm opening and checking the scenes in Edit Mode like this:

    Code (CSharp):
    1. public class SceneValidationTests
    2. {
    3.     [Test]
    4.     public void LevelScenes_Setup_HasValidSettingsObject()
    5.     {
    6.         // Load and iterate over all scenes in our special folder.
    7.         var sceneGUIDs = AssetDatabase.FindAssets("t: Scene", new[] { "Assets/MyScenes/" });
    8.         for (int i = 0; i < sceneGUIDs.Length; i++)
    9.         {
    10.             string path = AssetDatabase.GUIDToAssetPath(sceneGUIDs[i]);
    11.             var scene = EditorSceneManager.OpenScene(path);
    12.  
    13.             // Check that each scene contains a special object.
    14.             var specialObject = Object.FindObjectOfType<MySpecialMonoBehaviour>();
    15.             Assert.IsNotNull(specialObject);
    16.         }
    17.     }
    18. }
    This is much faster than Play Mode tests, but it works similar to Play Mode. You need to setup a test assembly for Play Mode tests and then change the code to load scenes as you would in your game via SceneManager.
     
  4. superpig

    superpig

    Drink more water! Unity Technologies

    Joined:
    Jan 16, 2011
    Posts:
    4,649
    I demoed pretty much exactly this scenario at Unite. The trick is to use a parametric test fixture, which you set up to get instantiated once for each of your scenes. You pass in the scene name/path/whatever in the constructor and store it, and then in the OneTimeSetUp you do the actual load of the scene. Then in each test you just do the check for that one scene.
     
    Xarbrough likes this.
  5. Xarbrough

    Xarbrough

    Joined:
    Dec 11, 2014
    Posts:
    1,188
    @superpig TestFixtureSource is exactly what I was hoping for! And I looked up the talk, it's an incredibly helpful and inspirational to get me started thinking about how to test things. Thank you! :) I already knew about parameterized test cases, but never knew what a TestFixture was used for.
     
  6. Xarbrough

    Xarbrough

    Joined:
    Dec 11, 2014
    Posts:
    1,188
    Edit: Solution below.

    After my first excitement about the TestFixtureSource and your example, I ran into a strange issue. My tests all used to pass, then I refactored them to use the TestFixtureSource following your Unite talk. I really copied the same approach, but for some reason the tests now randomly seem to fail without error messages and most of the tests actually appear as skipped.

    upload_2020-1-30_21-57-19.png

    Any idea what could be going wrong here?

    I'm having troubles finding an issue. If I remove some of the tests, all others pass again, but some of them are still skipped. If I remove other tests, again other tests randomly fail or are skipped.

    Code (CSharp):
    1. namespace Nementic.X.SoloMode
    2. {
    3.     using Nementic.X.Worldmap;
    4.     using NUnit.Framework;
    5.     using System.Collections;
    6.     using System.Collections.Generic;
    7.     using UnityEditor;
    8.     using UnityEditor.SceneManagement;
    9.     using UnityEngine;
    10.     using UnityEngine.SceneManagement;
    11.  
    12.     public class ZodiacScenesProvider : IEnumerable<string>
    13.     {
    14.         IEnumerator<string> IEnumerable<string>.GetEnumerator()
    15.         {
    16.             var sceneGUIDs = AssetDatabase.FindAssets("t: Scene", new[] { "Assets/Runtime/SoloMode/LevelSelectionScenes" });
    17.             for (int i = 0; i < sceneGUIDs.Length; i++)
    18.             {
    19.                 string path = AssetDatabase.GUIDToAssetPath(sceneGUIDs[i]);
    20.                 yield return path;
    21.             }
    22.         }
    23.  
    24.         public IEnumerator GetEnumerator() => ((IEnumerable<string>)this).GetEnumerator();
    25.     }
    26.  
    27.     [TestFixture]
    28.     [TestFixtureSource(typeof(ZodiacScenesProvider))]
    29.     public class SaveKeyTests
    30.     {
    31.         private readonly string scenePath;
    32.         private Scene scene;
    33.  
    34.         public SaveKeyTests(string scenePath)
    35.         {
    36.             this.scenePath = scenePath;
    37.         }
    38.  
    39.         [OneTimeSetUp]
    40.         public void LoadScene()
    41.         {
    42.             this.scene = EditorSceneManager.OpenScene(this.scenePath, OpenSceneMode.Single);
    43.         }
    44.  
    45.         [Test]
    46.         public void ValidateMultipleBoardsFilePathCorrect()
    47.         {
    48.             var multipleBoardsVariant = AssetDatabase.LoadAssetAtPath<LevelVariant>(
    49.                 "Assets/Runtime/SoloMode/SoloManagement/LV_ScriptableObjects/LV-02_MultipleBoards.asset");
    50.  
    51.             multipleBoardsVariant.hideFlags = HideFlags.DontUnloadUnusedAsset;
    52.  
    53.             Assert.That(multipleBoardsVariant != null, "No level variant source found.");
    54.  
    55.             foreach (var soloLevel in Object.FindObjectsOfType<SoloLevel>())
    56.             {
    57.                 // Ignore tutorials (indicated by presence of a guide).
    58.                 if (string.IsNullOrEmpty(soloLevel.guideCharacterKey) &&
    59.                     soloLevel.LevelVariant == multipleBoardsVariant)
    60.                 {
    61.                     Assert.That(soloLevel.FilePaths.Length == 1);
    62.                     Assert.That(soloLevel.FilePaths[0].Contains("Multiple_Boards"),
    63.                         AssertMessage("Incorrect file path for multiple boards.", scene, soloLevel));
    64.                 }
    65.             }
    66.  
    67.             Resources.UnloadAsset(multipleBoardsVariant);
    68.         }
    69.  
    70.         private static string AssertMessage(string message, Scene scene, Object context)
    71.         {
    72.             return $"{message} Context: '{context.name}'. Scene: {scene.name}.";
    73.         }
    74.  
    75.         [Test]
    76.         public void ValidateMultipleBoardsCorrectSettings()
    77.         {
    78.             var multipleBoardsVariant = AssetDatabase.LoadAssetAtPath<LevelVariant>(
    79.                 "Assets/Runtime/SoloMode/SoloManagement/LV_ScriptableObjects/LV-02_MultipleBoards.asset");
    80.  
    81.             Assert.NotNull(multipleBoardsVariant);
    82.  
    83.             foreach (var soloLevel in Object.FindObjectsOfType<SoloLevel>())
    84.             {
    85.                 if (soloLevel.FilePaths.Length == 1)
    86.                 {
    87.                     string ubongoPath = soloLevel.FilePaths[0];
    88.                     if (ubongoPath.Contains("Multiple_Boards"))
    89.                     {
    90.                         Assert.That(soloLevel.LevelVariant == multipleBoardsVariant, "Solo Level with incorrect level variant: " + soloLevel.name + " - " + scene.name);
    91.                         Assert.That(soloLevel.RandomizeBoardRotation == false, "Solo Level with incorrect RandomizeBoardRotation: " + soloLevel.name + " - " + scene.name);
    92.                         Assert.That(soloLevel.RandomizeBoardFlip == true, "Solo Level with incorrect RandomizeBoardFlip: " + soloLevel.name + " - " + scene.name);
    93.                         Assert.That(soloLevel.HasTimer == false, "Solo Level with incorrect HasTimer: " + soloLevel.name + " - " + scene.name);
    94.                     }
    95.                 }
    96.             }
    97.         }
    98.  
    99.         [Test]
    100.         public void ValidateZodiacTotalCrystalCount()
    101.         {
    102.             int expectedSum = Object.FindObjectsOfType<SoloLevel>().Length * 3;
    103.             var label = Object.FindObjectOfType<WorldmapCrystalCount>();
    104.             Assert.AreEqual(expectedSum, label.maxCount, "Mismatched crystal count for zodiac HUD: " + scene.name);
    105.         }
    106.  
    107.         [Test]
    108.         public void ValidateZodiacFinalLevelCrystalCount()
    109.         {
    110.             int totalSum = Object.FindObjectsOfType<SoloLevel>().Length * 3;
    111.             foreach (var gate in Object.FindObjectsOfType<CrystalGateLock>())
    112.             {
    113.                 Assert.Less(gate.crystalUnlockCount, totalSum - 3, "Gate unlock count must be less than total crystal count of all level minus the last one: " + scene.name + " - " + gate.name);
    114.             }
    115.         }
    116.  
    117.         [Test]
    118.         public void ValidateZodiacHUDCrystalCount()
    119.         {
    120.             var crystalCount = Object.FindObjectOfType<WorldmapCrystalCount>();
    121.             var nodeKey = Object.FindObjectOfType<SoloLevel>().saveKey;
    122.             string animalName = nodeKey.Split('/')[1];
    123.  
    124.             Assert.That(crystalCount.saveKey.Contains(animalName), "Zodiac HUD crystal count save key should contain the animal name as specified in the nodes save keys. Scene: " + scene.name);
    125.         }
    126.     }
    127. }
    After quite some investigation I found my issue. My example code only shows a couple of the tests, there are a few more methods which are very similar. The entire class is not refactored well, since I was only now starting to reorganize the code. In one of the tests, I opened another unrelated scene to test something, which used to work in the old structure, but now with the TestFixtureSource it breaks, since the scene reference suddenly becomes null. However, the issue was hidden quite well since it only appeared for specific ordering of the tests and sometimes not at all.

    Additionally, I was experiencing a visual glitch in the test runner, where some of the tests showed as failed, but without error message. When I toggled the display state of failed/passing tests in the top right corner of the runner window, the visuals were updated and showed the correct symbols again.
     
    Last edited: Jan 31, 2020