In 2014, I decided that one of my goals in 2015 was to start doing unit testing at work. I picked up a copy of The Art of Unit Testing and read a reasonable amount of it. I was excited about the idea of being able to automatically catch bugs, making it less likely that they’d slip through to be found by my code reviewers or testers.
Unit testing refers to automated tests designed to each test one “unit” – usually a function – in your code. This means checking that for any given input, the function will return the appropriate output. This is used before integration testing, in which the various units are tested together to ensure they work well together.
In 2015, every time I developed a new activity, I wrote C# unit testing code to go with it. When I worked on existing activities, I wrote C# unit testing code for them. Here’s what I learned:
1) Unit tests don’t catch bugs as much as they prevent bugs.
Long, complex functions are difficult to test; you have to ensure that you’re covering every possible path. Short, simple functions that do one thing are relatively straightforward to test. That means that to make testing easier, you end up changing the way you write code.
1a) Unit testing makes your code more modular.
Simpler code, of course, is less likely to contain bugs, which means that just writing code with unit testing in mind can improve your code quality…whether or not you ever actually write the tests!
2) Unit testing promotes the refactoring of existing code.
When you write unit tests, the test class becomes another client of the class under test, and it becomes appropriate to update the class under test to make it more testable. This means that writing tests for your existing code is a good excuse to go back and refactor that code, which means that if your team decides to add tests, you can use that as justification to spend developer time paying down your existing technical debt.
3) Unit tests take time now, but they save time in the future.
I’ve heard more than one person say that doing unit testing doubles the amount of time it takes to write code – after all, you’re writing twice as much code! I don’t write THAT much test code, but I’ve heard from people who do. You get some, but not all, of that time back later in the process, because you’ve made the testing process easier (if your tests accurately describe each unit, then you know it does what it says it does) and your code easier to read.
The real time savings, however, may come in when you need to make changes to the code later on. Having existing unit tests in place reduce the need to do regression testing; if you change a unit, then run the unit tests without errors, you know that you haven’t changed the expected behavior of that unit of code, and you can focus on doing integration testing for your new workflow.
Code is code; if you’re writing your unit tests and your code under test together and the unit tests fail, it could equally well be either set of code that’s buggy. That’s not even completely true; I find it’s usually the test that’s wrong, because I’ve missed that the code under test will only ever be called after some preconditions are met and I haven’t set up those conditions properly in the test code. Still, assuming you’ll find the problem whenever a test fails, if you’ve defined your conditions correctly, the only way to get a false negative (meaning the code is wrong, but the test didn’t catch it) is if both the test and the code under test are buggy, which is clearly less likely than only the code under test having a bug.
5) It’s good to make tests fail: the benefit of test-driven development.
When I started doing unit testing, I would write the code under test, then write unit tests to verify that code. Since I’d be calling the functions being tested from other functions I was developing, I’d generally catch bugs well before I actually added the unit tests that would capture those bugs.
I’m currently trying to move more towards test-driven development, where you write the tests first and then develop the code. In other words, first define the function signature and behavior (but don’t actually make it do anything), and then write the unit tests for that function. The tests will fail (because the function doesn’t do anything) and you can then add the actual functionality. If the tests pass, then you might have discovered a false negative and can fix the logic in your test before it trips you up running against the actual code.
6) Unit tests help you understand your code better.
This may be the largest benefit of unit testing for me. When I’m writing assertions against my code and the name of the function I’m testing doesn’t quite line up with the functionality I’m expecting, the assertions don’t quite make sense, and I know I need to rename the function (or a parameter). Additionally, unit tests are a way to document your code which is always correct; if your unit tests are all green (passing), then they should be accurately describing the functionality of the code. Unlike a comment, unit tests have to be updated when the code changes…at least, assuming you don’t allow code to be checked in with broken unit tests!
Is it worth it?
When I started writing unit tests, it was essentially because I was interested in trying them out. The company offered some training on how to write unit tests, but there was no mandate to actually use them. What I’ve found is that, while I’m not really finding bugs with my tests, they’re helping me to write cleaner code that will be easier to maintain in the future. For this reason, I’m now strongly encouraging the rest of the developers on my team to do unit testing as well.
In the past, we’ve had issues with functions and classes that get overly long and complicated. Now that we’re migrating our client code to .NET, I see unit tests as one way to encourage writing code that will be much easier to update in the future.