One post in and I've already taken the easy out on a title; oh well. In this so-far wonderful year of 2020, I'd set a goal for myself that I've failed to meet pretty much every year since I started making "resolutions": release some kind of software outside of my day job at TechSmith. I tend to make small chunks of progress here and there, bouncing around between ideas when the inspiration strikes before falling off and repeating the cycle in another month. This year hasn't been much different, but this time around I've also been focusing on taking care of myself better - eating healthier (occasionally), exercising (sometimes), and getting help with my depression and ADD (pills are easy!). Armed with these new barely-habits, I'm also going to write some blog posts. Here's one about unit tests.
That's a fair question, if the implied subject is "do unit testing in a side project" - otherwise, I don't understand the question, and I won't respond to it. Specifically, though, I want to try my hand at doing some Test Driven Game Development (TDGD). TDD is an oft-debated concept, but I've found that in practice, I'm pretty in favor of it. This post isn't about why you should try TDD if you haven't yet - you really should - so I'll just jot down a few things I like about it:
- Having to make a test pass before moving on helps keep your classes small because it's annoying to do on larger ones.
- You have an immediate consumer of your code, which will naturally guide your design so you don't frustrate yourself.
- In the end, there's a nice suite of tests lying around just itching to tell you something is broken.
The red means it's working!
Historically, I've always thought of game dev and unit testing as more or less exclusive. Maybe if you were writing a simulation game or something with a lot of algorithms, you could test the mathy bits, but bringing together input, animations, physics, and all that other state seemed like a nightmare to test - probably because it would be! The less state a class has, generally speaking, the easier it is to test. Perhaps more importantly, though, is that less state also tends to mean less opportunity for errors. When I've started to TDD a class that seemed difficult to test in the past, I've tried to figure out how to pull state out and simplify the test cases. So why not apply that to the aptly-named TDGD?
Now, just to make something clear, the main goal of TDD is not to have a bunch of tests; after all, unit tests exercising the isolated logic of our classes is great, but that doesn't necessarily mean these classes will place nice with each other - or with Unity's own systems. No, the primary goal of this test-writing exercise is to encourage smaller, more adaptable units of code (see the first two points above). This is really good for a game, because (in theory) it means we can prototype new ideas really quickly without worrying about breaking our existing stuff. Pretty neat.
The Unity challenge
Challenge might be a bit of an overstatement, but getting unit tests working with a Unity project definitely seemed daunting when I first started looking into it. Like many things, it actually wasn't too bad once I figured out the basics, but the documentation left a few things unclear, and while a cursory Google search gave some good results, I figured I'd put up my own take on it so I could say I officially started a dev blog.
The added complexity of the Unity engine is the first thing to sort through, but it shouldn't take too long. The first thing to note is that, as previously linked, Unity has a built-in test runner. The process for creating your first test is pretty well documented there, so I'll just sum it up here: right click in your Assets explorer and
Create > Testing > Tests Assembly Folder. Boom, done.
If you're like me, unfortunately, then there's already some confusion once it's created; Unity has tests split between Editor Mode and Play Mode. On the surface, they seem obvious, but I didn't quite understand what they meant in a testing context: was Editor Mode for editor-only scripts that extend Unity's functionality? Did I need to use Play Mode to use Unity systems? As it turns out, the difference is much clearer and, as far as I can tell, can pretty much be summed up like this: running your Editor Mode tests runs everything as-is, and running your Play Mode tests starts the game first - that is, it's more or less the same as you clicking the Play button in the UI before running them. I think that's actually what it does, unless you run it on a specific platform (I haven't gotten that far).
Another note to observe is the created
.asmdef file; creating a test folder also creates one of these bad boys. As a Unity hobbyist, I'd never had one of these in a project before, and that might be because most of my projects were too small to deserve one. From looking at the manual, Assembly Definitions placed into a folder specify that all scripts within that folder (and its children) are placed in their own assembly. If you've worked in Visual Studio before, I think it's a lot like a
.csproj. The neat thing about this is that it can isolate parts of your code from each other, and I assume tests get their own assemblies so it's easy to exclude them from the game executable.
One downside about this is that it isolates parts of your code from each other, which means they can't interact. This is pretty troublesome for tests that are supposed to exercise code outside of their assembly, and it wasn't immediately clear to me how to fix it - maybe I just missed something. In any event, here's how I solved it: more Assembly Definitions! I ended up with two root folders in my project,
Game (which has all of my shippable assets, there's probably a better name for this), and
Tests (probably self-evident). I then added an Assembly Definition in my
Game folder. Protip: use the GUID checkbox to make renaming assemblies easier. Once I had my two assemblies setup, I just had to reference the
Game code from the
Tests code. Easy enough:
Well, that's actually about it. From here on out, you're more or less just writing tests like any other framework. I haven't actually done much now that it's setup, but it seems pretty powerful - you can write tests as coroutines to execute frame-by-frame during Play mode, which could make testing all those gnarly interactions I mentioned before easier. There's even an official blog post talking about TDD using Play mode to make sure an object interacts with the world correctly.
There's still the question of where to draw boundaries between your code and Unity's, how to do things like mock input and such, but I don't think these topics are particularly Unity or even game dev specific. My approach so far is using the humble object patter: I create a regular old C# class that contains all of my logic, then create a MonoBehavior that calls into it and provides data from Unity stuff. I don't love how it shook out, but it's the first TDD thing I've done with Unity so that's maybe a bit expected; you can peep the code on Github.
There was something else I wanted to discuss here, but I didn't know where to put it, so I'm sticking it on the end. Writing code usually isn't easy, and writing clear, concise, extensible, and maintainabile code is at least half the profession. When you're working on a game, or even just a one-off project, it's pretty easy to want to ignore those principles and just make something. I have absolutely nothing against that; practices like TDD are typically geared towards long-lived projects, especially projects that will have multiple generations of developers. There's nothing wrong with writing code the quickest and easiest way you can to get a dude jumping on platforms and collecting coins, and trying to write code "the right way" is always going to slow you down and, often times, distract you from the problems you're actually trying to solve.
On the other hand, like all skills, it'll get easier over time with practice and dedication. My hope is to build up the well-tested and flexible outline of a game with components I can reuse and adapt with ease. There's a decent chance I won't make it that far, and that's fine too, but I figured it was worth mentioning that this is definitely an experiment for me. If you read this and have any question, comments, or suggestions, my contact info is to your left!