Testing, and writing testable code. Part 1

Published Apr 18, 2018Last updated May 27, 2018
Testing, and writing testable code. Part 1

It's hard to believe now, but I didn't seriously test my software until about three years ago when I started my current job. TDD seemed like a lot of unnecessary ceremony.

Why write three times the code? Safe to say, my opinion on the matter has changed. When you operate in an environment where you have a window to safely deploy your changes, you want to make sure those changes are safe.

So, why write tests?

If you are a hobbyist just coding for fun, I don't have a great answer for that. If there are only a few people that you want running your software, then not having a test suite isn't a huge deal. But here are some of the reasons I have decided to write tests for everything but the most trivial projects.

  • It shows others how your code is supposed to work. If you are writing a library for other people to use in their code, tests serve as excellent examples of how somebody might use your software. It's a living manual. If you are working on a team, it will show the others on the team what your intent for writing the code was.

  • You can quickly identify breaking changes. You are tasked with adding a new feature to an existing service or application. How do you know that your new code won't break your old code? If a new change is breaking old tests, you have to decide if there is a bug with the feature, or if the tests need to change. Either way, once rectified, the tests will represent the new state of your code.

  • Time! You will save a lot of time tracking down bugs if those bugs never make it out to the end user. Either way, you are still testing. If you are fixing bugs reported by the user, it can be tricky. If they are nice they may include the stack trace which can be helpful in determining where in the software the bug was introduced.

Unit testing!

Writing tests may seem like a daunting proposition, but you can do it! It's the same way you tackle any large task, one piece at a time.

Consider the following function.

public static class Fizzer{
 public static string FizzBuzz(int i){
  if(i % 3 == 0 && i % 5 == 0){
   return "FizzBuzz";
  }
  if(i % 3 == 0){
   return "Fizz";
  }
  if(i % 5 == 0){
   return "Buzz";
  }
  return i;
 }
}

If we pass in 1 or 2, we may expect it to return 1 or 2. If we pass in 3, we would expect to get back Fizz. If we pass in 5, we may expect to get back Buzz. Fifteen should give us FizzBuzz.

We have just described the correct output of the test, now let's put our assumptions to code.

[TestMethod]
public void FizzBuzz_CheckOutput(){
 Assert.AreEqual("1", Fizzer.FizzBuzz(1));
 Assert.AreEqual("2", Fizzer.FizzBuzz(2));
 Assert.AreEqual("Fizz", Fizzer.FizzBuzz(3));
 Assert.AreEqual("Buzz", Fizzer.FizzBuzz(5));
 Assert.AreEqual("FizzBuzz", Fizzer.FizzBuzz(15));
}

Each Assert will throw an exception if the expected output is not equal to the result. If no exceptions are thrown, the test passes.

If we get a new business requirement such as, if i is divisible by 10 we need to return Pop!, what is going to happen when I change the method to look like this?

public static class Fizzer{
 public static string FizzBuzz(int i){
  if(i % 3 == 0 && i % 5 == 0 && i % 10 == 0){
   return "FizzBuzzPop!";
  }
  if(i % 3 == 0 && i % 5 == 0){
   return "FizzBuzz";
  }
  if(i % 3 == 0){
   return "Fizz";
  }
  if(i % 5 == 0){
   return "Buzz";
  }
  if(i % 10 == 0){
  	return "Pop!"
  }
  return i;
 }
}

We may change the test to look like this.

[TestMethod]
public void FizzBuzz_CheckOutput(){
 Assert.AreEqual("1", Fizzer.FizzBuzz(1));
 Assert.AreEqual("2", Fizzer.FizzBuzz(2));
 Assert.AreEqual("Fizz", Fizzer.FizzBuzz(3));
 Assert.AreEqual("Buzz", Fizzer.FizzBuzz(5));
 Assert.AreEqual("Pop!", Fizzer.FizzBuzz(10));
 Assert.AreEqual("FizzBuzz", Fizzer.FizzBuzz(15));
 Assert.AreEqual("FizzBuzzPop!", Fizzer.FizzBuzz(50));
}

And this will reveal a bug in our code. Assert.AreEqual("Pop!", Fizzer.FizzBuzz(10)); will fail, because instead of Pop!, it will return Buzz. It checks for divisibility of 5 before divisibility of 10. It doesn't handle the case of if it's divisible by both. In fact, since every number dividable by 10 is also dividable by 5, you will never see Pop! by itself.

Because this is an arithmetic error, it's not going to cause a compile time issue or a runtime issue. There are only two ways something like this would get caught, manual testing, or unit testing.

The next post will go into detail on how to write non-trivial tests, and further on, how to write more testable code.

Discover and read more posts from Joel Longanecker
get started