Testing. I Finally Get It.

Something I rarely see in coding tutorials is how to test your code. In fact, the first time I ever came across testing was when I started reading the Rust Book, which was well over a year after I started coding. There are likely a number of reasons for this, some specific to me and some not, but I suspect it has something to do with the fact that writing tests doesn't actually add any functionality to what you've built. It just adds overhead when you're trying to learn or trying to teach.

I didn't think I needed tests at first. My programs were small. If they didn't run correctly, it was easy to track down the bug or rewrite the part causing errors. Learning how to write tests seemed like a waste of time when I was struggling just to get things working. And if I'm being honest, this is still largely the case. My side projects haven't broken into thousands-of-lines-land yet and I haven't had to refactor them to add functionality that wasn't originally planned. But is that really a good reason to forgo learning how to write tests?

The Catalyst

At work I get the chance to work on a code-base that is not just in thousands-of-lines-land, but in millions-of-lines-land. The landscape is... different. The question is no longer just "does this function work?", but "does this function work, how does it fit into the system, and how do I ensure that it continues to work when other areas change"

A few months ago I ran into a situation where the result of a calculation was incorrect, but only in certain situations. It was easy to locate the bug in the code. The trouble was making sure that when I fixed it, I didn't break something else. It wasn't the problem itself that was difficult to solve. It was the quantity of different input combinations and variations I had to account for that made it tough. Trying to keep all of the different variables in my head and the tedium of manually testing each one was mind-numbing. I needed a better way to do this, but I didn't know where to start. I needed to learn how to write tests. Plain and simple.

First Step on the Road to Enlightenment

Unit tests were my first step. A unit test isolates a small portion of the code, like a function or method, and attempts to test it for correctness. Reading the Rust Book was the first place I found examples of how to write unit tests, so I felt it was only fitting that I write my first tests in Rust. Rust makes it easy to write tests by allowing developers to put tests in the same file as the code itself.

We start with a simple struct and its impl block:

For those unfamiliar with Rusts syntax, a similar Java class might look like this:

public class RGB {
    int red;
    int green;
    int blue;
  
    public RGB(int red, int green, int blue) {
        this.red = red;
        this.green = green;
        this.blue = blue;
    }
  
    public String hexCode() {
        return Integer.toHexString(red) + Integer.toHexString(green) + Integer.toHexString(blue);
    }
}

To add unit tests, all we have to do is use the #[cfg(test)] annotation below the RGB impl block and create a new module called tests.

Within the tests module we added use super::* to get access to everything in the parent module, our RGB struct and impl block. To add a test we annotated a function with #[test] and then wrote the test. It doesn't matter what the name of the function is, but it should be descriptive of what it is testing. Depending on the size of the module there may be varying amounts of setup involved, but unit tests tend to boil down to checking whether an expected result is the same as the actual result. We use assert statements to do this. In our example, we used rust's assert_eq! macro to assert that "F0CBD1", our expected result, is equal to rgb.hex_code(), our actual result.

As we add methods to the impl block(s) we'll continue to add tests to the tests module. In my colors project I have a generic method that takes an implementation of any color type and returns an implementation of any other type. I'll leave the details of it's setup for another time, but the method signature is fn to(&self)<Type> -> Type. To test it, we check whether each of the properties of the new color type is what we expect after conversion. We set the test up the same way, and then write an assert statement for each property in the struct:

In this test, we created a color with RGB type properties, our expected result, and created the same color with HSL type properties to test our conversion. We converted the HSL type to the RGB type using the to method. Then we checked each property of our converted HSL against our original RGB. If any of the assert statements fail, the test will fail.

So how does this fit into the bigger picture? Back to my earlier problem. I believe that spending a little extra time to create a unit test for each of my input combinations will save me hours down the line. It will allow me to focus completely on the problem at hand instead of having to stop every time I make a change to make sure something didn't break. Not only that, but unit tests can act as a road map for implementing functionality, each test a pit stop on the road to completion.

To Infinity and Beyond

As the software changes, previously implemented unit tests can act as regression tests. Regressions tests help us confirm that all parts of the system continue to function as expected when changes are introduced elsewhere. So any time a change is made in the system that could touch my code, instead of manually checking every possible combination and variation of inputs...again, all I have to do is run my tests to make sure I'm in the clear.

Rust makes writing unit tests easy. But what do we do if our language doesn't have tests baked in? I think another reason we don't see a lot of tutorials for testing is because it requires too much extra setup that isn't within the scope of the tutorial. But there are plenty of great testing frameworks out there. A great way to get started is to take a look at the tests on any Code Wars Kata (and even write your own). You can find information on the Code Wars Test Framework here. For JavaScript apps, there are plenty of options: Jest, Jasmine, Mocha, Chai, Enzyme. JUnit is a pretty standard testing framework for testing Java applications. I've heard NUnit is similar for the .NET ecosystem.

Unit tests won't save us from every type of bug. Edward Dijkstra famously said "Program testing can be used to show the presence of bugs, but never to show their absence!" But I think it's a great place to start.

Update:
In a hilarious, awesome, and terrifying turn of events, I found a bug in my code! Adding a new combination of RGB values to my test_rgb_to_hex_code() test will cause the test to fail:

The hex_code() method works fine, but it doesn't handle small values correctly. What we really want when we have RGB(1, 2, 3) is the hex color code 010203, but what we get actually get is 123. This is definitely not what we want, especially since a hex color code of 123 is more likely to be thought of as the short form for 112233 rather than 010203. Looks like Dijkstra was right! Time to refactor.

Reference:
Rust Book, Jest, Jasmine, Mocha, Chai, Enzyme, JUnit, NUnit, Code Wars Test Framework