What is the Business Value of Unit Testing? Part 2

Unit testing continued - dealing with changing requirements.

In the previous post, we started on a business application with the requirement:

As a user, I need a way to enter a number. Once the number has been entered, I need it to be printed back to me.

The customer loves the app! But has a few new requirements for us:

As a user, I need a way to enter a number. Once the number has been entered, I need it to be printed back to me.
If a number evenly divisible by 3 is entered, the user should receive back “Fizz” rather than the number
If a number evenly divisible by 5 is entered, the user should receive back “Buzz” rather than the number

The requirements now are equivalent to the children’s problem and/or code kata FizzBuzz

Our code previously only had a single branch of logic. Get number, return number. Based on the new requirements, we can see there will be a few more branches:

  1. number is not evenly divisible by 3 or 5
  2. number is evenly divisible by 3
  3. number is evenly divisible by 5
  4. (not explicitly stated but) number is evenly divisible by both 3 and 5.

The 4th branch was not stated by the requirements, but seem like something that should be asked of the business owner, as it might not have been considered, or could have even been assumed.

Our original method which looked like:

1
2
3
4
public string ReturnNumberAsString(int numberToReturn)
{
return numberToReturn.ToString();
}

Will be updated to now look like:

1
2
3
4
5
6
7
8
9
10
11
public string ReturnNumberAsString(int numberToReturn)
{
if (numberToReturn % 3 == 1 && numberToReturn % 5 == 1)
return "FizzBuzz";
else if (numberToReturn % 3 == 1)
return "Fizz";
else if (numberToReturn % 5 == 1)
return "Buzz";

return numberToReturn.ToString();
}

Now be aware, all of our unit tests from last round, continue to pass, as the data that was being used to test the method continues to pass with our new implementation. This is often something that is brought up as a potential pitfall of unit testing and requirements changing - and it is seemingly a valid concern! Our unit tests are continuing to pass, when the requirements are much more complex than they were previously.

This is why it’s so important to take into account *both* unit tests (and their asserts) as well as code coverage. There is always the possibility that the unit tests *won’t* break as a result of new branches in your code. BUT, if you were to look at code coverage, specifically the code coverage as it applies to our method, you’ll see that not all branches of code are covered by unit tests.

As you can see from the above screenshot, our code coverage as indicated by the percent, and the purple and yellowish text has gone down. The 3 new branches within our code is not currently being covered by unit tests. Here’s the repo as of updating the method, but without the unit tests to cover the requirements: https://github.com/Kritner/UnitTestingBusinessValue/tree/bb1f9bda9250fbdb85a8737c0c006f06e6daa788

Now to write a few unit tests:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/// <summary>
/// number mod 3 and 5 returns FizzBuzz
/// number mod 3 returns Fizz
/// number mod 5 returns Buzz
/// </summary>
[TestMethod]
public void NumberReturner\_ReturnNumberAsString\_SpecialCasesReturnValid()
{
// Arrange
NumberReturner rt = new NumberReturner();
int modThree = 9;
int modFive = 10;
int modThreeAndFive = 15;

// Act
var resultsModThree = rt.ReturnNumberAsString(modThree);
var resultsModFive = rt.ReturnNumberAsString(modFive);
var resultsModThreeAndFIve = rt.ReturnNumberAsString(modThreeAndFive);

// Assert
Assert.AreEqual("Fizz", resultsModThree, nameof(resultsModThree));
Assert.AreEqual("Buzz", resultsModFive, nameof(resultsModFive));
Assert.AreEqual("FizzBuzz", resultsModThreeAndFIve, nameof(resultsModThreeAndFIve));
}

Hmm. We currently have a failing test. Fizz is not being returned from resultsModThree, but 9 instead. Let’s see what’s going on here.

Oh. Looks like I’ve inadvertently created a bug in my implementation of requirement #2.

1
2
3
4
5
6
if (numberToReturn % 3 == 1 && numberToReturn % 5 == 1)  
return "FizzBuzz";
else if (numberToReturn % 3 == 1)
return "Fizz";
else if (numberToReturn % 5 == 1)
return "Buzz";

Should have been:

1
2
3
4
5
6
if (numberToReturn % 3 == 0 && numberToReturn % 5 == 0)
return "FizzBuzz";
else if (numberToReturn % 3 == 0)
return "Fizz";
else if (numberToReturn % 5 == 0)
return "Buzz";

Now that we’ve corrected the code, our new unit test passes. But our original unit test:

1
2
3
4
5
6
7
8
9
// Arrange  
int expected = 42;
NumberReturner biz = new NumberReturner();

// Act
var results = biz.ReturnNumberAsString(expected);

// Assert
Assert.AreEqual(expected.ToString(), results);

Is now failing. Of course it is - 42 % 3 is 0, so we actually received a Fizz for 42. Updating that test to have an expected value of 7 instead.

What does all of this mean? Our unit tests both helped us and hurt us in this scenario. They helped us because they helped us determine I had a logic error in my implementation of a requirement. They hurt us because we had a “false positive” pass. This is why it’s so important that unit test Asserts are relevant, and code coverage stays high. Without a combination of both of these, the business value of the tests is less significant. The updated implementation and logic: https://github.com/Kritner/UnitTestingBusinessValue/tree/78f03b8550593b9576f28e8608561f4add989879

In a non unit testing scenario, it is likely that our business logic would only be testable through the UI. UI testing is much clunkier, slower, and harder to reproduce consistently. Imagine after each change of our logic, we had to test all branches over and over again through the UI. This probably means a compile, a launch, an application log in, navigate to your logic to test, etc. Oh, and then do it three more times (due to this **simple** application’s logic). This is another reason unit testing is so powerful. As we keep making changes to our code, we can help ensure the changes we’re making are not impacting the system in ways we’re not expecting. And we’re doing it in a fraction of the time that it would take to do the same thing through manual UI testing.

Hopefully this post and the previous help show how a good unit testing suite can really help you not only have less bugs in code, but get your code tested much faster.

Related:

Author

Russ Hammett

Posted on

2016-02-22

Updated on

2022-10-13

Licensed under

Comments