The Philosophy of Unit Testing

Unit testing is an approach to software development which shifts from testing the entity as a whole to testing each unit of code at every level until you end up testing the whole. Small issues don’t snowball into larger issues if you catch them early. Less has to be reworked at each level of abstraction when tested correctly. You trade a little bit of speed in the development cycle in exchange for a much smoother, more predictable process.

Unit testing won’t catch each and every bug, but that’s not its purpose. When you test on a unit basis, you are testing whether that specific unit functions as expected. We don’t care about the actual code in the method, just that each element in our input set matches each expected result from our predicted output set. If there’s a mismatch here and you continue, what happens when another piece takes into account that behavior as intentional?

Unit testing is part of the process and isn’t the entirety of testing. You start with unit testing, then move onto how each unit is integrated with integration testing. Is your breakdown happening in class a or the helper library? Is your API time out impacting your gatherer? How does everything work together?

From here, you move on to systems testing. The goal at this phase is to make sure that all of the integrated modules and pieces function as a system. Just because the code’s right doesn’t mean the system works as intended. Then finally you move onto real world testing as acceptance testing. Is your product ready for launch or to the next phase of production? As you progress through the stages, unit testing still remains essential, but it shouldn’t be the focus.

How Does Unit Testing Fit Into the Whole?

Unit testing is the foundation of your testing. The underlying philosophy behind unit testing necessitates that you are able to divide individual parts of your program into discrete units. You have to be able to control the input and the predict the output. This requires a more in depth understanding of the intention behind each unit. One of the first places this becomes notable is a new programmer’s first calculator program. If you define a basic division method as:

function divide ( x, y )
	return x / y
end

What happens when y is 0? Some languages, like Lua return inf or similar, but others will crash. Depending on where this is used in your program and what y could be, you can be looking at a lot of issues if you don’t address either the input or the way it’s handled. You understood what you were doing, but you didn’t test where your algorithm can break down. Unit testing is the most fundamental piece in ensuring that best practices are both possible and practical at all levels of your project.

Dealing With Bad Input

If we look at the division example before, we have 3 ways to address the input of y as zero (though the third may not be practical on its own in certain languages). We can either adjust how the function is used by adjusting the raw input, we can adjust the handling in the function, or we have predetermined output which only occurs with errors. Each of these three methods provides a different way to solve the same problem, but has different limitations.

If you have an unexpected behavior in a function, you can adjust how it is used in practice. Sanitize the input before it reaches your function. This is usually the easiest solution for the functions, but can convolute the dispatch of said function. The more places your function is used, the more places you need to completely control the input. This is fine when the input needs to be sane for other reasons anyways, but makes less sense when the function is used all over the place in different ways. You have an array of theoretical solutions to this problem by adding abstraction, but each layer slows down the overall functioning of your program and your project.

For most division functions, you would want something to check if your y is 0 or reduces to 0. The issue with adding handling in our function for something like our division is then what do we return when the divisor is 0? You either need to have multiple pieces of data returned (usually for strongly typed language), or some specific output to flag an error, and what it is. If you have to handle this anyway, why not bake it into the error handling in your unit of code? Most languages aren’t as forgiving as Lua for dividing by 0, just like how some can make sense of 1 + cat where Lua can’t. With something like Lua or Perl, you could return pretty much any scalar variable and have handling where it’s called.

Verifying your output may not be necessary with the proper safeguards and trivial enough code, but it becomes more important when you work with complicated systems. Even though we may have verified the input, we want to make sure that the data returned doesn’t become an issue for the next unit. Checking the output is arguably just checking the input for another unit.

Complicating Things for Simplicity’s Sake

It sounds like we’ve complicated the system, but what we’ve done is complicate the basic workflow to make a more scalable solution. However we designate our error structure for our division function means we can do similar with our other math functions in our calculator project. We have to provide some kind of handling to interpret this output, but this can be abstracted away.

You can create a table, a struct, or whatever data structure makes sense. I like to use a struct in C# which includes a bool and an error code so that if I need more than just the success, I have something else for further handling. Did we have an error from input or somewhere further in? Now, I know and can handle accordingly. This is a trivial example, but it makes up the highest percentage of my C# returns.

If this becomes baked into your error handling, it becomes baked into your unit testing. An error may not be an issue with your code, it may be that a third-party API is down when testing. Complex error handling makes more and more sense for non-trivial code and makes testing make more sense. When I check where I break down, I can rule out issues with connecting to a glitchy API, timeouts, and similar without wasting time looking at other code.

Optimizing Units

Breaking down errors and fault points is only one part of unit testing. As you progress further in the process of development and as you get further through integration testing various parts, you want to make sure your code is optimized. While you may not be concerned with Big O notation at this point, you’re still concerned with making the most common tasks run faster.

What is costing your program execution time? Using likely and unlikely analytics on input can help determine where and to what extent you should handle optimization. Is your function more likely to return true than false? If so, weed out the false’s to the very end. Now, your function runs faster for one set of conditions than the other. This may not help with rarely used functions, but can add up quickly for common operators.

How do you test each unit and address execution speed? To even begin, you need to know what is executing what, how often, and where it loses its time. Most languages have frameworks for these sorts of tasks, but you need to work them into your unit testing framework after you’ve made the function fulfill its necessary purpose in the developmental pipeline. There’s no point optimizing for speed if you don’t even know what exactly your function should be doing, or when it’s holding back further development.

Applying Unit Tests

Unit tests are extremely important to modern software development for anything non-trivial. While only unit testing is not enough, it is the foundation of almost any solid testing strategy. To apply unit testing, look at what each composes an individual unit.

Once you know what constitutes a unit, you can determine what types of input and output it deals with. A math function is going to deal with numbers of some sort, a text parser with strings, etc., but you need to know exactly what you’re working with. Does your math function take integers or doubles? What does it return?

What edge cases exist? For division, you have the issue of trying to divide by zero. You also have huge numbers divided by extremely small numbers causing an overflow depending on what type your result is. What can break each unit and how do you prevent it from breaking the program? Or, should it break the program? Sometimes, the error is bad enough or improbable enough it’s okay to just let it crash. If you get complete gibberish in a method which should be protected or private that isn’t your doing (e.g. you offer the source), do you even care?

As you design unit tests, you have to know how to break down your expectations for your units. What input will you see? What input might you see? Finally, what input is completely impossible? How does your program handle each of these? Even if you don’t intend to correct some of them, it’s best to know what they do and test on them.

Some code may end up depending on specific error behaviors. Some bug fixes may unintentionally fix certain edge cases which other pieces rely on. Changes in process may make an error condition impossible which wastes lines and cycles somewhere else in the program. Proper unit testing sets you up for each evolution of the development cycle to function more efficiently and more scalably at the expense a little extra work in the beginning. It won’t fix everything, but it’s just the first step towards doing everything the right way.

Image by Paul Brennan from Pixabay