Unit Testing Games: Tip #2

Keep tests fast

Tip #1 is about keeping unit tests small. This is greatly complemented by keeping them fast.

Unit tests are a powerful tool for your development team, and time is possibly the teams most valuable asset. Having tests that execute rapidly both promotes their use, and removes barriers to running them in the heat of Alpha. If an engineer can execute 300 tests in less than 2 seconds, they will likely get run. If 100 tests take 20 minutes, they will almost never be executed.

Remember not to confuse size with speed. Small tests will very likely be fast, but that’s not a guarantee. Let’s look at our “small” test from Tip #1.

def test_weapon_fire(self):
    # setup
    # =============================
    # This version will NOT implicitly load assets on construction
    weapon = Weapon("rifle")
    start_position = vector(2.0, 0.0, 2.0) 
    # execute
    # =============================
    # Reduce ammo
    # Note: we no longer require a target
    weapon.fire(start_position, None)
    actual_ammo_post_fire = weapon.get_current_ammo()
    # assert
    # =============================
    # Verify we lost ammo on fire
    self.assertEqual(weapon.get_max_ammo() - 1, actual_ammo_post_fire)

Depending on the details of the Weapon class constructor and the fire() method, this test could take a second or two to execute. It’s plausible the constructor loads art assets from disk. This could be for the weapon itself, as well as for it’s ammo. It’s also plausible the fire() method performs line-of-sight checks, which can be computationally expensive.

Having this test execute in 1-2 seconds may not be an issue in isolation. It becomes a problem when we add more tests. Adding tests for reloading (another 1-2 seconds) and switching (2-4 seconds to load 2 weapons) raises the tally to 4-8 seconds. When we have 100 tests (100-200 seconds) or, even better, 600 tests (600-1200 seconds), you see how this rapidly increases.

The Weapon class could be refactored to remove asset loading, maybe by moving this to a load() function. This could reduce construction to simple variable initialization, which takes milliseconds. If you can reduce the time to 100-200 milliseconds, you can now get through 100 tests in 1-2 seconds instead of 1. 600 tests now execute in 6-12 seconds instead of 20 minutes.

Something is better than nothing

It may not be reasonable to refactor like this for speed increases. That’s OK. The primary goal is to add reliability to your systems, which increases overall productivity. I will always prefer a single, slow, test over no tests at all!