Why test your code and why should we care? Well, it's very popular approach nowadays, to write a code that tests your code, just to be sure everything works as expected (and it's hard to keep track of everything with large codebase). It's common misconception that's not feasible for games, after all, how can you test something as complex as interactive program - and even worse - with multiple players?
The answer is that you're not writing a code to test your whole game. You're not gonna write a bot that plays it, does all the quests and check if everything still works (and even looks for holes in the walls, though that'd actually be cool). You write the code to test only portions of it, the small pieces, the units. Hence the name: unit testing. There's of course lots of materials on it over the internet, so we're not going to dive into the details, I just describe what were the problems, and how we solved it, so that we can finally try unit tests.
Isolating the code
When you look at the code you'd like to test, first thing that come to mind is that it's probably too complex and involves too much dependencies to be easily testable. Let's look at the example:
bool critter_use_item(Critter& cr, Item& item, Critter@ targetCr, Item@ targetItem, Scenery@ targetScen, uint param)
bool useOnSelf=(not valid(targetCr) && not valid(targetItem) && not valid(targetScen))
if(FLAG(item.Flags,ITEM_RADIO) && useOnSelf)
Say we need to test that when player uses the radio item, he can edit its settings. Unfortunately, to invoke critter_use_skill int this context we need:
Due to above limitations, unit testing your code seems just not feasible. Surely, there must be some solutions, we just need to go out and learn how the world is doing this.
The concept is simple. If you need to provide something for the tested code, that's not the part of the test itself but constitutes to the test context, provide a mock. A substitute for real object, a substitute for a function that's gonna allow us to isolate the tested code to the form that may be safely run from within unit test context. As far as above example goes, we would need three mocks:
- player - Critter object with overriden IsPlayer() method that's returning true (another mock actually), or with fields set in a way that IsPlayer() is returning true
- an item - Item object with fields telling us it's a radio
- a function to detect that EditRadioSettings has been called
Mock libraryAt first I wanted to write some library that would be able to load the script, compile it and execute test functions providing overriden implementations for functions we wanted to mock. However, the AngelScript engine does not allow to re-register any function, so that once we've got our engine set up, we can't register any mocks. We thought of some workarounds, but after some time I decided to modify the server source directly (its angelscript source, to be precise), I suspected it would be very minor modification - so that maintain costs are minimal in the future (keeping it up to date with every server update).
Tests Runner serverAfter some fiddling in AngelScript engine code, I found out that I can easily 'redirect' function call to another script function (whether the original call was meant to invoke script function, or native (engine registered) one. Moreover, it turned out that method calls can be simply redirected to function calls (providing the first argument is the object passed), without any extra work! This way call to bool Critter::IsDead() could be handled by bool critter_IsDead(Critter& cr). Nice!
How does it affect our testing capabilities? To put it simply, we ended up with solution that may be used for testing generally, not only for unit tests. Unit tests are designed to test your small pieces of code out there, but hence we are running our tests in full fledged server, we may as well test the system more broadly, we may check how different pieces interact together - whether it works as a whole or not (though I admit, it wasn't my initial goal - I just wanted simple unit testing facility, not integration tests).
But let's get back to the example. Let's start from the last mock we needed, namely, a function substitute for EditRadioSettings. Say, we just want to know, whether the function has been called or not. Let's mock it, and make it so our substitute will indicate that's been called:
For above example to work, we need to know what CallExpectation function is doing. It simply increases the call counter, that's stored in some dictionary under the index that's been passed as argument. Later on, we may check that the counter is equal to 1 - that means our function has been called as expected. With such dictionary at hand, we should also define more helpers: Expect(funcname) and VerifyExpectations(). First one is used to remember the fact, that we want the function funcname to be called, and the latter (called at the end of the test), will check it. In fact, we should have the ability to specify the numbers of the call we're expecting:
- ExpectOnce(funcname) - VerifyExpectations will succeed only if function has been called once
- Expect(funcname, count) - success only if called count times
- ExpectNonce(funcname) - success if hasn't been called at all
This function will remember that we want to redirect the call to EditRadioSettings to a function mock_EditRadioSettings.
Ok, what about other mocks? We said that we want an item, and a critter with specific properties and/or we may provide mocks for their methods as well. For this I've implemented simple MockCritter and MockItem functions that spawn the needed objects with only basic properties filled, but for the case of this example, let's assume those are just clean objects - with all properties zeroed out, rest will be handled by method mocks:
And now, our test function (with needed mocks) in full glory:
Critter@ player = MockCritter();
Item@ radio = MockItem();
radio.Flags = ITEM_RADIO;
// call tested function
critter_use_item(cr, radio, null, null, null, 0);
Voila! Now, when we call critter_use_item, it will first call Critter::IsPlayer(), which returns true, even if our Critter structure might have not indicated this, but our mock did. Later, it will check the flags (we've set it on our mocked item) and call the EditRadioSettings, which in fact calls mock_EditRadioSettings. This sets our expectations counter to 1, which is then verified by VerifyExpectations. And then it announces success - we've got our first trivial unit test passed!
The solution we've got is in very early stage, I'm gonna try and involve it in some of the 2238 code testing. We will see if it turns out to be useful.
Enough of this mockery for now!