Search Unity

Question Test with a SerializeField that is an array

Discussion in 'Testing & Automation' started by JJBocanegra, Oct 11, 2022.

  1. JJBocanegra

    JJBocanegra

    Joined:
    Jan 7, 2022
    Posts:
    2
    I have a MonoBehaviour class with this property, initialized as an empty array by default.

    Code (CSharp):
    1. [SerializeField] private GameObject[] enemiesPrefabs = new GameObject[0];

    And now I have a test class to test this MonoBehaviour
    Code (CSharp):
    1.  
    2. var spawnManager = new GameObject();
    3.     enemySpawnManager = spawnManager.AddComponent<EnemySpawnManager>();
    4.  
    5.     var enemyPrefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Prefabs/Enemy/Enemy 1.prefab");
    6.     // var prefabInstance = Object.Instantiate(enemyPrefab, new Vector2(0, 0), Quaternion.identity);
    7.  
    8.     var so = new SerializedObject(enemySpawnManager);
    9.     so.FindProperty("enemiesPrefabs").arraySize = 1;
    10.     so.FindProperty("enemiesPrefabs").GetArrayElementAtIndex(0).objectReferenceValue = enemyPrefab;
    11.     // so.FindProperty("enemiesPrefabs").managedReferenceValue = new GameObject[1] { prefabInstance };
    12.     so.ApplyModifiedProperties();
    13.  
    I tried everything, from using the instance of the prefab to adding it as an array of prefabs, but nothing seems to work, I have a
    Debug.Log(enemiesPrefabs.Length)
    in the code and it is always zero.

    Any suggestions?
     
  2. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    6,005
    From the code I posted, this is the only line of code using enemiesPrefab and it instantiates it with an empty array:
    Code (CSharp):
    1. [SerializeField] private GameObject[] enemiesPrefabs = new GameObject[0];
    Did you add game objects in the inspector?
    Did you assign an array that has items in it?

    If neither is the case, the list will be how you initialized it: empty.

    Also, you can't just make an array have a size of 1.

    And why are you trying to work with SerializedObject rather than accessing the array directly? Is this GUI code?
     
  3. Xarbrough

    Xarbrough

    Joined:
    Dec 11, 2014
    Posts:
    1,188
    I don't have the time to test your case and I don't see why it's not working, but my advice would be to also change your test implementation.

    Currently, you're trying to setup private data from the outside. This per-se is already something to avoid in tests. Rather than testing against the private parts of the implementation, you want to test against the public interface and the contract an object provides. In some situations this still requires an object to be set up in a specific way.

    You can use a mocking library such as NSubstitute to create fake objects which provide canned responses for example. For MonoBehaviour this won't work, so instead, you can also do it oldschool:
    • Just make the property public so it can be modified. Maybe not because it breaks encapsulation.
    • Make the property internal and use InternalsVisibleTo to make it settable via tests but not code in a different assembly.
    • Make it protected and create a child class for use in your tests. The test class can implement a public setter that calls the protected property.
    Maybe you don't want to build the object in code, but setup a complete prefab with prefab references assigned in the inspector and simply load that prefab in your test. Why not create prefabs only for tests?
     
  4. JJBocanegra

    JJBocanegra

    Joined:
    Jan 7, 2022
    Posts:
    2
    It's the first case, I add the enemies prefabs from the inspector, and it works as it should, even if initialized empty.

    About the array size I just added that option experimenting, I want to know how can I modify a serialized property that is an array, because every other value is easy to modify but I can't find anything to do it with an array.

    I can't access the array directly because it is private, and using the SerializedObject allows access to the SerializeFields properties.

    And what is GUI code? I'm really new to Unity.

    But it is a SerializeField, is private but it allows to be modified from the inspector, so it should be tested no? Is also a core part of the script. I have tested a lot of other scripts with SerializeField properties with no issues, but I'm really stuck doing it when is an array.

    The script is an enemy spawner, and it take the enemies from the enemiesPrefabs variable and spawn them randomly.

    About the last question, do you mean creating a prefab purely for tests purposes? If that's the case it doesn't sounds good to have something only for the tests no?

    Also maybe I'm doing something wrong and a prefab can be added to an script by other means that is not a SerializedObject?


    Thanks both for your replies!
     
  5. Xarbrough

    Xarbrough

    Joined:
    Dec 11, 2014
    Posts:
    1,188
    In my experience testing in Unity is better done by moving the testable logic away from MonoBehaviour and into easy to test classes. For example:

    Code (CSharp):
    1. public class EnemySpawnerComponent : MonoBehaviour
    2. {
    3.     [SerializeField]
    4.     private GameObject[] prefabs;
    5.  
    6.     private SpawnStrategy spawnStrategy = new RandomSpawnStrategy();
    7.  
    8.     private void Start()
    9.     {
    10.         spawnStrategy.Spawn(prefabs, transform);
    11.     }
    12. }
    13.  
    14. public abstract class SpawnStrategy
    15. {
    16.     public abstract void Spawn(GameObject[] prefabs, Transform parent);
    17. }
    18.  
    19. public class RandomSpawnStrategy : SpawnStrategy
    20. {
    21.     public override void Spawn(GameObject[] prefabs, Transform parent)
    22.     {
    23.         int index = Random.Range(0, prefabs.Length);
    24.         Object.Instantiate(prefabs[index], parent);
    25.     }
    26. }
    27.  
    28. public class MultiSpawnStrategy : SpawnStrategy
    29. {
    30.     public override void Spawn(GameObject[] prefabs, Transform parent)
    31.     {
    32.         foreach (GameObject prefab in prefabs)
    33.         {
    34.             Object.Instantiate(prefab, parent);
    35.         }
    36.     }
    37. }
    38.  
    39. public class SpawnStrategyTests
    40. {
    41.     [Test]
    42.     public void MultiSpawnStrategy_SpawnsAllPrefabs()
    43.     {
    44.         var prefabs = new [] { new GameObject("A"), new GameObject("B"), new GameObject("C") };
    45.         var parent = new GameObject("Parent").transform;
    46.        
    47.         var strategy = new MultiSpawnStrategy();
    48.         strategy.Spawn(prefabs, parent);
    49.        
    50.         Assert.AreEqual(3, parent.childCount);
    51.     }
    52.    
    53.     [Test]
    54.     public void RandomSpawnStrategy_SpawnsSinglePrefab()
    55.     {
    56.         var prefabs = new [] { new GameObject("A"), new GameObject("B"), new GameObject("C") };
    57.         var parent = new GameObject("Parent").transform;
    58.        
    59.         var strategy = new RandomSpawnStrategy();
    60.         strategy.Spawn(prefabs, parent);
    61.        
    62.         Assert.AreEqual(1, parent.childCount);
    63.     }
    64. }
    There's no reason to unit-test the EnemySpawnerComponent here. There's no interesting logic in it. If you want to test the interaction with other systems ("it puts stuff into the scene before the player manager is initialized"), you can implement an integration test which uses either a real prefab from the game or a fake one with data in it and then checks if the GameObjects were correctly spawned after some time. However, this test would already be very brittle, since you may want to change such behaviour in the future. But maybe you have good reasons to ensure that the behaviour never changes (regresses), because you know it might cause a bug if the spawner waits 2 seconds before spawning for example.

    The more interesting logic that is better to test can be moved into classes which you can more easily modify and use on their own. There's no reason why you shouldn't give those classes public properties or constructors that take the prefab array for example.

    Also see:
    https://blog.unity.com/technology/unit-testing-part-1-unit-tests-by-the-book
    https://blog.unity.com/technology/unit-testing-part-2-unit-testing-monobehaviours