Unit tests are one of my favorite things to write as a developer. I simply love them. It’s like a little game I get to play with myself to ensure I’ve covered all possible scenarios and verify that my code is absolutely bulletproof. If I’m doing maintenance work on a project without tests, you better believe it won’t be without tests by the time I’m finished. And when I review another developer’s code and find issues that I know would’ve been caught by even the simplest of unit tests, I feel sad.
There’s been a lot of buzz about unit tests for several years now, but it’s still a challenge getting everybody to buy in. Where’s the real benefit with these tests? Isn’t it going to take me a lot longer if I have to write tests for everything? Aren’t I just making more work for myself when I make the changes that I’ll inevitably be making? No, no, NO!
The benefits of unit tests and test-driven development are so great and so many that whether or not to do it shouldn’t even be a discussion. That said, I realize there’s a path to enlightenment that takes time to travel, and I want to help you get there. So let’s explore some of these endless benefits that I’m speak of…
Code that is difficult to test is often a result of poor design. If you follow the SOLID design principles, you will find that your code generally lends itself well to unit tests. Writing tests first can help identify a bad design before it’s too late. SOLID principles make testing easier. So, by embracing unit tests and test-driven development, your coding practices will naturally evolve to follow these principles.
- Single responsibility principle – Testing and verifying an object with a single, clear, defined purpose will be easier than testing a class with multiple, vague responsibilities. Know your class’s purpose, and you’ll have a better idea what tests to write. It’s important to apply this principle to methods, as well. A method that does one thing is easy to test. A method that does 100 things, not so much.
- Open/closed principle – Making classes extensible will make them flexible in unit test scenarios. One of my favorite techniques is using partial mocks with RhinoMocks, which rely heavily on this principle.
- Liskov substitution principle – Embracing this principle will help you keep base classes and interfaces simple. This will make consumers of your objects easier to test because less time can be spent repeating unnecessary mocking. Don’t make an IMyDataProvider that has implementation-specific public methods (e.g., CreateDatabaseConnection(), ExecuteStoredProcedure(), etc.). Instead, create a method that does what’s needed, like GetData(). The result is that a MockMyDataProvider can be provided, and you need only mock the GetData method every time instead of each implementation-specific method. Still write tests for those implementation-specific methods, but write them in the tests for your data provider implementation!
- Interface segregation principle – Interfaces are, in my opinion, the single most important aspect of unit testing layer interactions. Mock objects built from interfaces are absolutely essential for testing most non-trivial scenarios.
- Dependency inversion principle – Creating mocks, stubs, and fakes is meaningless without a way to inject them into the object being tested. Use constructor injection, property injection, or a dependency injection framework like Unity to provide your object with mocks to remove outside dependencies and test specific functionality.
There is nothing more frustrating than spending hours upon hours troubleshooting an issue only to arrive at a piece of code that has clearly never been run. Write tests, and check your code coverage. If there are lines of code that haven’t been covered, you can evaluate them to determine whether or not additional tests should be written. Play the what-else-could-possibly-go-wrong-here game with yourself. Enforce your own assumptions with exceptions, and test those. If you expect an argument to be not-null, write a test that expects an ArgumentNullException to be thrown; that will be significantly easier to troubleshoot than a null reference exception.
If you’ve spent any significant time maintaining the work of others, you know this is a big one. It’s unsettling to find yourself in the middle of someone else’s 1000-line function with a block of commented code that looks like it does exactly what’s been reported to not be happening. Clearly, removing it was an intentional move, but what will you be breaking by uncommenting it? If a test is written (MammothMethod_ShouldNotCallDatabaseDirectly()), and you uncomment the code that calls the database directly causing that test to fail, you’ll have received two yellow flags warning you that maybe this is a bad approach. Without the test, a developer shrugs, uncomments the code, feels good about it, and re-introduces a bug that was likely fixed previously by another developer. (Didn’t we fix that two releases ago?) Writing tests for your fixes ensures that you aren’t on the other end of that scenario where somebody undoes your fixes. (If they still undo your fix and break or refactor your test in the process, be sure to yell at them.)
Unit tests are also helpful in terms of code management when complex branching strategies are employed. Let’s say you have 20 developers working in different branches. When the branches are brought back together, how can you confirm that changes were not lost? With unit tests in place, you simply need to run the tests. Since that should be happening automatically with each build anyway, all you need to do is verify that the test are there. This really beats the alternative of reviewing code to see if a particular change is included. Plus you feel good as a code-mover knowing that the code is verifying itself with each move.
Well-Tested, High Quality Solutions
At the end of the day, what you’re really trying to produce is a quality solution. You want your deliverables to work correctly when your assumptions are met and handle unexpected scenarios gracefully. You want to tell your users how your application will handle any situation they can come up with. You want your users to feel great about you, whether it’s your company, your product, or you specifically. Unit tests can help you do all this. What happens if the web service isn’t available? There’s a unit test to verify an email is sent. How can I audit queries to the database? There’s a unit test to verify logging. What if an expected record is missing? There’s a test for that, too. If you can think of it, make sure you’ve tested it. Write the tests as you go, and you’ll be sure not to affect features implemented earlier. At the end of development, your code will have a collection of tests self-verifying every feature and enforcing all assumptions.
Did it take more code? Yes.
Did it take more time? Arguably.
Will the final product have higher quality, require less maintenance, and result in a net decrease in total development effort over time? Definitely.