TDD Series: To Write A Test
Let’s say with the last entry I have convinced you that you should learn test driven development. Wouldn’t it be nice if I was right and there was an easier way? Well in that case you need to start. Where do you start with test driven development? Well with a test. If you haven’t read my last entry the company I worked for allowed me to post my testing utility scripts in this github repository:
https://github.com/Insight-Via-Artificial-Intelligence/UnityTestUtilities
The readme has instructions on how to get started with the test runner and the custom unity package. For the moment though let’s talk about the basics. Our goal is to be able to easily test each component in the system in isolation. The ideal case of this is one testing script per monobehaviour script. Now some scripts will rely on other scripts and we might even be using [RequireComponent(typeof(differentScript))] to enforce two monobehaviours to be on a GameObject. That’s okay, we can just test those scripts together as one ‘component’. Our goal is to find ways to write small, easy to maintain tests that will tell us something useful about our code.
There are two kinds of tests you can run with the test runner. Edit mode tests and playmode tests. In edit mode tests you have access to all of the editor code as well the normal code. I’ll admit that I don’t know a lot about edit mode tests. I haven’t ever really needed this ‘editor code’ - for instance I have used the edit mode only AssetDatabase functions in my runtime tests. I assume it is essential if you need to test complicated editor scripts - but if you have read this post you will know I generally use very small functions in my editors (I have started using editors again after investigating UI elements and I need to write about it). So as you might have guessed I write Playmode tests. When we run these tests the unity test runner creates a temporary scene for you. This scene persists until all tests are complete and then the test runner destroys the scene (unless it doesn’t and they just pile up at the top of your project - in that case the best thing to do is to delete the scene and restart unity). So you have an empty scene to do whatever you want for your test.
It is then that we run into our first problem…
How do I ah… get something to test?
So here we have this empty scene… Do I want to load one of my scenes?
The answer is… maybe? You can make integration tests - tests which automate the way a user would play your game (in comparison to testing functions on your scripts). If that sounds complicated that is because it is - especially compared to what the type of tests I suggested that you aim for before. So naturally this is the first road I went down with Illic. Are these “integration” tests useful? Yes - I have a nice system which plays through levels and checks you still win (though it cheats and bypasses the UI to make moves). It is especially nice to be able to have your computer play the healing level (if only one test fails 99% it is that level) fifty times to save your sanity. Now this is pretty useful but it has a major flaw, even if I know that the healing level is broken, I have no idea how it is broken. It is a lot better than no tests, but it still takes a lot of time to have the game run through the level.
So let’s take a step back. Before we worry about integration tests, let’s consider unit tests. That is testing a single unit of code which is what I was talking about in the beginning with simple tests for testing a single ‘component’. In that case we are back to where we were before. How do I get this single component? It is easy enough to:
GameObject testObject = new GameObject();
ScriptWeAreTesting testingScript = testObject.AddComponent<ScriptWeAreTesting>();
But what about if we just want to instantiate a single prefab? Now you can do a lot by making fresh components, but chances are if you haven’t been doing test driven development you haven’t designed your scripts like that. The other reason to want to instantiate a prefab is to test how it handles serialised variables. Either way we will need to do something like this to get the prefab:
string path = “Assets/Prefabs/CharacterPrefabs/Enemies/Archer”
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
This is verbose, will quickly clutter your tests and you will quickly get sick of it, especially since that is before the instantiate call. This is where my helper scripts come in. My TestAssetLoader has some nice static functions to help you out.
How do we use it?
The first step is to clone the repository here, add the assembly it is in to your test assembly and use this namespace “IVAI.EditorUtilities.Testing”. Now with all of that done we create an instance of the prefab like this:
SampleUnit sampleUnit = TestAssetLoader.CreatePrefab<SampleUnit>("SampleUnit", "CharacterPrefabs/Enemies/Archer", testObjects);
Now if you don’t have your prefabs in the folder “Prefabs” or you have a folder between Assets and Prefabs that is fine. If you look in the readme I have instructions on how to configure the asset to look in the right place (reading from a text file with the names of the folders to look into).
The next thing you might notice is that extra argument called “testObjects” what is that all about? It is a list of GameObjects, the reason why is simple. At the end of each test we want to clean up after ourselves. When I first started to write unit tests I would keep having my tests randomly fail when I ran all of them at once, but when I ran the failing tests by themselves they passed. I scratched my head for a while but the answer was simple. I was testing some scripts with colliders and since I was not deleting the GameObjects they persisted in the testing scene. Which meant that as soon as I made the next object the OnTriggerEnter would fire on the last tests objects and since I was testing enter counts fail the test. So I started manually adding each GameObject to a list and destroying them in the test's TearDown method (called exactly once at the end of every test). That was both a pain and guess what? Sometimes I forgot and tests would randomly fail and it might not be apparent that was the cause since the tests would fail so quickly. So I refactored my test script so it will automatically add anything you make during a test to the given list. Then you call CleanUpObjects in every TearDown method on the list and everything will be deleted.
So how does a test look with this in mind? Something like this:
// Tells the test runner it is a test.
[Test]
public void HelloWorldTest()
{
//Create the unit to test
SampleUnit sampleUnit = TestAssetLoader.CreatePrefab<SampleUnit>("SampleUnit", "", testObjects);
// Call a function string helloWorld = sampleUnit.HelloWorld();
// Assert something
Assert.AreEqual("Hello World", helloWorld);
}
As you can see, that is a pretty small and easy to write test. The test is already small, but I can do better if I make use of the SetUp method (called once at the start of every test) to create the instance and make the test:
[Test]
public void HelloWorldTest()
{
string helloWorld = sampleUnit.HelloWorld();
Assert.AreEqual("Hello World", helloWorld);
}
This test isn’t really a good test (comparing strings like this can be fragile) but that is for a future blog entry to talk about. What is good is that it is small. It isn’t much effort to write a couple of these and before you know it you will easily push a thousand tests. Now there is a lot more to say about writing good tests that are useful, but that can be for another day. Just knowing that a test becoming giant is a bad thing means you have skipped months of headaches of trying to write and maintain elaborate tests. With this out of the way next time I can talk about the process I use to write tests. I am sure you can’t contain your anticipation.
Until then, I hope you enjoy your August.
Get Lords of Illic
Lords of Illic
Are your tactics good enough to become a Lord of Illic?
Status | Released |
Author | OrangeDrake |
Genre | Strategy |
Tags | Singleplayer, Turn-based Strategy |
More posts
- Version 19.044 days ago
- Version 18.666 days ago
- Domain Driven Design66 days ago
- Version 18.5Sep 03, 2024
- Burn away the ThornsAug 18, 2024
- Second Hand Musings Part 3: ShareableAug 13, 2024
- Version 18.4Jul 16, 2024
- Version 18.3May 27, 2024
- AIE LivestreamMay 13, 2024
- Version 18.2Apr 22, 2024
Leave a comment
Log in with itch.io to leave a comment.