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

Best practices for testing prefabs?

Discussion in 'Testing & Automation' started by falconfetus8, May 30, 2021.

  1. falconfetus8

    falconfetus8

    Joined:
    Feb 4, 2013
    Posts:
    17
    I'm interested in adopting Test Driven Development for my next Unity project. I've already hit one major snag: loading prefabs during my tests. That's a big problem, because my player object is a prefab.

    Obviously, I can just put my player prefab inside the Resources folder and use Resources.Load<GameObject>(), but that's apparently a bad thing according to Unity's official tutorials.

    This is what I'm doing at the moment:
    Code (CSharp):
    1.  
    2.     [TestCaseSource(nameof(VariousLeftStickValues))]
    3.     public void Player_Speed_Is_Proportional_To_Left_Stick(Vector2 leftStick)
    4.     {
    5.         var input = Substitute.For<IInputService>();
    6.         input.LeftStick.Returns(leftStick);
    7.  
    8.         var gameObject = MakePlayer(input);
    9.         var player = gameObject.GetComponent<OverworldPlayerController>();
    10.         var motor = gameObject.GetComponent<OverworldMotor>();
    11.  
    12.         // Move forward by a frame and assert that the motor's speed has been
    13.         // set correctly
    14.         var expectedVelocity = new Vector3(
    15.             leftStick.x,
    16.             0,
    17.             leftStick.y
    18.         );
    19.         expectedVelocity *= OverworldPlayerController.WalkSpeed;
    20.  
    21.         player.Update();
    22.         Assert.That(motor.Velocity == expectedVelocity);
    23.     }
    24.  
    25.     private static IEnumerable<object[]> VariousLeftStickValues()
    26.     {
    27.        /* Don't concern yourself with this. */
    28.     }
    29.  
    30.     private GameObject MakePlayer(IInputService input = null)
    31.     {
    32.         input ??= Substitute.For<IInputService>();
    33.         var time = Substitute.For<ITimeService>();
    34.  
    35.         var prefab = Resources.Load<GameObject>("Prefabs/OverworldPlayer");
    36.         var player = prefab.GetComponent<OverworldPlayerController>();
    37.         var motor  = prefab.GetComponent<OverworldMotor>();
    38.  
    39.         // TODO: Use Zenject to wire these up, instead of setting them manually
    40.         player._input = input;
    41.         motor._time = time;
    42.  
    43.         return prefab;
    44.     }
    45.  
    It works, but it's still using Resources.Load().
     
  2. superpig

    superpig

    Drink more water! Unity Technologies

    Joined:
    Jan 16, 2011
    Posts:
    4,649
    Do you intend to run your tests on devices, or only inside the Editor? If the latter, you can use AssetDatabase.LoadAssetAtPath to load the prefab you want.

    If you want to run the tests outside the Editor, then what I suggest is that you make 'fixture scenes' for your tests - i.e. a scene with your Player prefab (as well as any other objects that might be needed for the tests), which you then load in the setup phase of your tests. The scene can be left out of your build settings and then dynamically included by a
    TestPlayerBuildModifier
    processor; that way, building your game for normal distribution shouldn't be affected by the test machinery.
     
  3. falconfetus8

    falconfetus8

    Joined:
    Feb 4, 2013
    Posts:
    17
    I've tried using LoadAssetAtPath, but it always seems to return null, no matter what I do. This is a playmode test by the way, but I'm running it in the editor.

    Also, is it OK if I call LoadAssetAtPath in _every_ test, performance-wise? Does it cache the asset if it's called multiple times?
     
  4. superpig

    superpig

    Drink more water! Unity Technologies

    Joined:
    Jan 16, 2011
    Posts:
    4,649
    As long as the code is running in the Editor, it shouldn't matter whether you're in playmode or not. Most common reason for it to return null is that you're asking for the wrong asset type; maybe try
    LoadMainAssetAtPath
    instead?

    Yes, if the asset is already in memory then an existing reference will be returned. That said, in the interests of keeping your tests simple, I'd probably just do it in a [OneTimeSetUp] method, and also have a [SetUp] method which actually instantiates the prefab. That way your individual tests can focus on just operating on the instance, instead of repeating the setup.
     
    falconfetus8 likes this.
  5. falconfetus8

    falconfetus8

    Joined:
    Feb 4, 2013
    Posts:
    17
    I tend to dislike [SetUp]-style methods in unit test frameworks, because it implies that there will be state that can persist between tests---state that I must remember to reset in a [TearDown] method. The last thing I want is to accidentally make my tests dependent on each other.

    That being said, I'll definitely use [OneTimeSetUp] to load the asset, since that's not a very stateful thing. Thanks for the tip!

    Actually, the problem turned out to be that I left out the ".prefab" at the end of my asset path. This surprised me, because the behavior of Resources.Load is to _ignore_ the ".prefab" extension, and I expected LoadAssetAtPath to be the same.

    I used this snippet to determine the _actual_ path of my asset.
    Code (CSharp):
    1.    
    2.     private GameObject _playerPrefab;
    3.  
    4.     [OneTimeSetUp]
    5.     public void LoadAssets()
    6.     {
    7.         string[] searchResults = AssetDatabase.FindAssets("OverworldPlayer");
    8.         string prefabPath = AssetDatabase.GUIDToAssetPath(searchResults[0]);
    9.         Debug.Log(prefabPath);   // Prints "Assets/Prefabs/OverworldPlayer.prefab"
    10.  
    11.         _playerPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
    12.     }
    13.  
     
  6. superpig

    superpig

    Drink more water! Unity Technologies

    Joined:
    Jan 16, 2011
    Posts:
    4,649
    I guess it's a matter of style, but it's kind of funny to me, because I share your goal of not accidentally making tests dependent on each other, and yet I see [SetUp] as a way to guarantee that in a lot of cases :)
     
  7. falconfetus8

    falconfetus8

    Joined:
    Feb 4, 2013
    Posts:
    17
    The way I see it, [SetUp] is only useful if you have some state that is shared between each of your tests. You'd use it to ensure the field is always set back to a consistent starting point. If you're going to be resetting it between every test, then you may as well not have the state be shared in the first place. If you're _not_ resetting it between tests, then it's possible for one test to contaminate another.

    That's just my limited viewpoint though.
     
    GRASBOCK and superpig like this.