As _Dungeon Life_ gets old - I've been working on it for over two years now! - I have been starting to feel the bite of manual testing. As your product gets larger the number of cases you have to test increase, but I'm only one person without a testing department, and the ratio of time spend manually testing to time spend actually creating is getting bad. (It's that not-fun kind of testing, too: is the tutorial still working? Can I still buy things? Can I still switch skins? Does the werewolf still transform?) So I've decided it's time to switch to a test first approach.
Back in 2008 when I was working on Schizoid for the 360 I did the whole thing test first - https://www.gamedevblog.com/2007/08/test-driven-dev.html - and I swore I wouldn't go back. And yet, truth be told, I did -- it was just too important to me to get to that Minimally Viable Product or prototype to what, for me, was two-three times longer to get there. I'm reading Software Engineering At Google and I like their quip about software engineering being programming integrated over time. The amount of process and software engineering discipline you apply to your code depends on how long you want your code to last--so I stand by those decisions to not do Test First with Bejewelled Blitz Live, Sixty Second Shooter, Castleheart, Legend of You, Dungeon Life and various prototypes. (What about Energy Hook, a project that took me four years? Maybe it would have been appropriate for that. Though working on a single-player game in C# with Unity was a much less bug-prone environment than the other games...)
Dungeon Life has gone past that two year mark. If I had known I would still be working on it I would have done TDD from the get go. So now I need to start applying the lessons from Martin Fowler's Working Effectively With Legacy Code on my own codebase.
I'm _not_ attempting to the get the whole thing under test before starting new work. That would be no fun; I'd never get to add a feature again. Rather, I'm being a good scout and trying to leave the campground in a better state than I found it. And as I get started I'm finding just how hard it is to get Roblox stuff under test. Ok, they have a test framework TestEZ, which will work with Typescript and Lua, and...I'm not using it. Rather I'm essentially just putting a bunch of assertions in various test scripts and loading them one by one (if they all went simultaneously results would be unpredictable, because a lot of them operate on the main Player - which is its own problem) presented here in glorious typescript:
const runTests = true
const currentTest = undefined // "ClickableTests" //"MeleeWeaponTests" // "GameServerTests" // "SuperbossTests"
if (runTests && game.GetService("RunService").IsStudio()) {
warn("Running Tests")
for (let moduleScript of script.Parent!.Parent!.FindFirstChild<Folder>("TS")!.FindFirstChild<Folder>("Tests")!.GetChildren()) {
if (moduleScript.IsA("ModuleScript")) {
if (!currentTest || currentTest === moduleScript.Name) {
warn("Running " + moduleScript.Name)
TestUtility.setCurrentModuleName(moduleScript.Name)
require(moduleScript)
}
}
}
warn("All tests run")
}
And then run it in studio.
One of my earliest discoveries is this is a super pleasant way of developing. I had forgotten what it was like to have a tight inner loop; the minutes it could take to run Dungeon Life and get to the part I wanted to manually test could be agonizing after several failed attempts to fix something. Being able to put the code I want to test right at the start of execution is huge, and worth doing TDD for almost by itself.
On the negative side, I discover I've made lots of what are effectively globals: services and managers and namespaces with their own static data. Just the easiest thing to do at the time. In particular, I'm finding a table I keep of player-relevant data, and the service that manages inventory for players, is used throughout the code. There are multiple ways I can tackle getting these under test. I could make a more object oriented Inventory class on a one-per-player basis and start passing those around the codebase; I could make the Inventory Manager a class rather than a namespace and have both a Test and Production version that I pass to functions; I can follow the Demeter Principle and instead of accessing the global from the function pass the often one piece of information I care about into the function. Why not all of the above? The smallest refactor is the second thing; I turn a couple of my commonly used namespaces into classes that I pass to functions. In some classes and functions I find myself passing in too many parameters now, so I'm adopting this Context design pattern: https://accu.org/index.php/journals/246, Because generally I either have that test context or a production context; I'm liking this pattern a lot. Sometimes to keep the number of parameters down I pass it into a class's constructor where it can hold a record. The pure functional coder in me feels a twinge when I do this; I'm duplicating data, violating DRY, but these are immutable references so I don't feel too bad about it. (Using readonly when I can, keeping it private with an accessor.) And the other things I can do when they're useful or make more sense.
Another thing is Roblox's Player class - I'm using it _everywhere_. I often use it as the key in various tables. My first thought is to make proxies but that's a place where Roblox Typescript bites me; it doesn't support get/set, so code that directly refers to a field on a player won't necessarily work with a proxy. (I believe there's a way to do it with Lua but it's not worth switching back for.) So if I _do_ one day go the proxy route I'm going to have to refactor all the field accesses to use accessors instead. I took a brief stab at this and didn't get there. Right now I'm doing the low-tech sketchy thing of just having the tests operate on Player zero (or one, if you're counting in Lua) and so far it's been fine but I know eventually that will be untenable. I'm going to have to suck it up and get that proxy in, someday.
In the meantime though, the places where I'm using Player as a table key? I don't have to use Player for that. I can generate a key for the player--as long as I make sure to give them a new one if they leave and come back. I learned the hard way, years ago, to not use player id's as keys, because if a player leaves and comes back and I've forgotten to clear them from the table hilarity can ensue.
In my previous article about Roblox best practices I said my favorite thing for code reuse was to have a script in my object require or import a module script. It turns out getting that under test is not easy! We could write some bindable functions to access the guts of those objects, and we can do 'integration' style tests where we just examine the object while its internals are a black box, but say I want to write tests to cover if an object gives a player loot when they click on it. It's now looking like I should either use the CollectionService to attach code to those objects (and then in test I can just attach it directly) or maybe when the objects are created I attach the code then (and can put a test around the creation function.)
It's taking me a while to come up with these solutions. I flounder around, try things, they turn out to be a lot of trouble, I try other things. But I'm getting there. If you're interested, you can see some of my latest stabs at it on github. https://github.com/JamieFristrom/dungeonlife
It's amazing how often programmers have to remember that TDD (and BDD) are good things to do even if they are annoying. I know it's something I have to face every day when writing back ends for Angular apps.
And even then I still can't often figure out my own old code.
Posted by: Tipa | August 26, 2020 at 10:56 AM