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(), but that’s apparently a bad thing according to Unity’s official tutorials.
This is what I’m doing at the moment:
[TestCaseSource(nameof(VariousLeftStickValues))]
public void Player_Speed_Is_Proportional_To_Left_Stick(Vector2 leftStick)
{
var input = Substitute.For<IInputService>();
input.LeftStick.Returns(leftStick);
var gameObject = MakePlayer(input);
var player = gameObject.GetComponent<OverworldPlayerController>();
var motor = gameObject.GetComponent<OverworldMotor>();
// Move forward by a frame and assert that the motor's speed has been
// set correctly
var expectedVelocity = new Vector3(
leftStick.x,
0,
leftStick.y
);
expectedVelocity *= OverworldPlayerController.WalkSpeed;
player.Update();
Assert.That(motor.Velocity == expectedVelocity);
}
private static IEnumerable<object[]> VariousLeftStickValues()
{
/* Don't concern yourself with this. */
}
private GameObject MakePlayer(IInputService input = null)
{
input ??= Substitute.For<IInputService>();
var time = Substitute.For<ITimeService>();
var prefab = Resources.Load<GameObject>("Prefabs/OverworldPlayer");
var player = prefab.GetComponent<OverworldPlayerController>();
var motor = prefab.GetComponent<OverworldMotor>();
// TODO: Use Zenject to wire these up, instead of setting them manually
player._input = input;
motor._time = time;
return prefab;
}
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.
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?
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.
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.
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
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.