Unit Tests and a Space Rescue Mission

I have seen countless people preaching that unit tests are very important for any software project. The fact that very few of them actually back up their arguments with convincing examples make unit tests look like sad, old school dogma that can be safely ignored. People are creature of examples, we won’t really believe something until we see an example up-close.

Today, I once again make this argument: unit test is critical for ensuring your code works as intended. Unit tests don’t seem that useful because it’s indeed not very useful the first time you write your code. But we are not living in a static world, we live in a marketplace where business logic can change for multiple times in hours, we live in a world where tomorrow we can wage wars against today’s friends. Changing circumstances means evolving codebase. You will almost certainly go back to part of your software and modify it to do something different, however slightly. Unit test really shines when change happens: it will let you know when your new change accidentally breaks some other part of your application.

To aid my argument, I invite you to an adventure far away into the future.

The year is 2148, entire earth is in shock of a horrifying news: the Martian settlement has been taken over by our lethal space foe — The Zerg. And all of our Martian settlers have been taken hostage.

Terrified and furious, you, the CTO of the space-grade weapon manufacturer Space Force, decided to deploy a fleet of your most advanced alien-fighting robots in one of the most epic space rescue mission the world will see.

You swiftly jogged to your computer and started typing out a program on which your ferocious robot fleet will run. You chose a programming language that was open-sourced more than a century ago.

Mars will have two species when your robots arrive: human and Zerg. So you need two models to represent them. Since both of them are organic, you create two structs that conform to the same Bio protocol.

The Bio protocol dictates that any Bio unit will have two properties in common: they will have certain number of legs and a body temperature (in celsius). According to your company’s elite scientists who have been studying this ruthless alien species, each Zerg will have 8 legs and their body temperature will be 27 celsius.

protocol Bio {
var bodyTemperature: Double { get }
var numberOfLegs: Int { get }
}
struct Human: Bio {
let bodyTemperature = 37.0
let numberOfLegs = 2
}
struct Zerg: Bio {
let bodyTemperature = 27.0
let numberOfLegs = 8
}

Now you have defined models you need, you move on to add some code to your sophisticated Zerg killing machine.

According to the data you got from your science staff, you confidently coded up your Robot’s logic. The advanced computer vision algorithm and thermal sensors on your robots can easily recognize a bio’s number of legs and the body temperature. So you go ahead and create a method to determine whether a bio is a friend or a foe based on these properties. In this method you check the following: if a bio unit has more than 2 legs or its body temperature is less than 30 celsius, then it’s a disgusting Zerg to eliminate.

struct RescueRobot {
func isTarget(bio: Bio) -> Bool {
return bio.numberOfLegs > 2 || bio.bodyTemperature < 30
}

func encounterBioTarget(bio: Bio) {
return isTarget(bio) ? kill(bio) : rescue(bio)
}

func kill(bio: Bio) {
// unleash horrible weapon … code has been omitted.
}

func rescue(bio: Bio) {
// do something nice … code also has been omitted.
}
}

In a hurry, you, who does not believe the importance of having unit tests, are ready to ship your robots to Mars and save the day.

5 minutes before the launch of the robots-carring space ship. You got an emergency phone call from your science staff. She informed you that the measurement was off, and the body temperature of Zerg is actually higher than they thought. It turns out that instead of 27 it’s 31 celsius. “Easy fix”, you calmly said.

You opened up your editor and made some quick changes like a pro:

func isTarget(bio: Bio) -> Bool {
return bio.numberOfLegs > 2 || bio.bodyTemperature < 40
}

After reboot all your robots, they are sent into space, along with hope of the entire planet.

I didn’t go well.

Your robots wiped out the entire planet. They eliminated all the Zergs as instructed, but failed to rescue a single human because humans were also slaughtered mercilessly by your cold-blood machines. How could it be?

Had you had your unit tests in place, this tragic would have been avoided entirely.

Let’s say you had two unit tests to verify that robot will kill Zerg but will not kill human.

func testIsTargetWhenEncounterHuman() {
let human = Human()
XCTAssertFalse(rescueRobot.isTarget(human), “Human should not be target”)
}

func testIsTargetWhenEncounterZerg() {
let zerg = Zerg()
XCTAssertTrue(rescueRobot.isTarget(zerg), “Zerg should be target”)
}

“testIsTargetWhenEncounterHuman” passes before your modification but will fail after it.

As you can see, changing circumstances in real world will often force you to modify your logic in many ways. By having unit test as a verification of your software’s intended behavior will save you time, trouble and sometimes lives. It might not seem very useful in your first iteration, but it provides necessary sanity check and invaluable confidence whenever you come back to your codebase.

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.