I hate debugging my code.
The process is more than just a frustration. It reminds me of my original solution’s inadequacy. It requires me to waste spend time reviewing code I’ve seen probably
a million a lot of times already, in hopes that I’ll miraculously stumble onto the error. All this effort spent in vain devours the excess time in my schedule, so I end up having to deliver something less awesome than I really wanted to create. Or, even worse, it runs out the clock so much that I miss my deadlines and I have to scramble.
yeah. I hate bugs.
They are by far the most unpredictable element of a time schedule. The project can have a ridiculously solid definition of its requirements, and still have no legit quantifiable prediction for the amount of time it’ll need for the debugging phase! How can one accurately estimate how much time it will take to resolve an issue that is, by definition, deceptive against the guy who’s writing the code? The best laid plans of mice and men… yada yada.
Bugs kill me.
So, after several rounds of butthurtedness from my code (and my faulty, entirely human brain), I have sought a better way. There must be a better approach to coding, I thought, that takes a more proactive approach to the development cycle with respect to debugging.
Turns out, there is!
What is test-driven development (TDD)?
Test-driven development, or TDD for short, is a stupidly simple concept: test your code before you write it. You write your test criteria first, then you write the function that will successfully pass that criteria. When you work this way, you’ll be testing at the same time you’re developing — so it doesn’t become an afterthought, it’s not half-assed, it’s not cast off as an annoying distraction. Testing and documentation can actually be fun!
It describes your program in terms of tests
A pretty cool side-effect of testing before you write is that you can actually describe your program in terms of the test criteria — a super efficient way to protect against feature creep.
Feature creep is Definitely. Not. A Good Thing ™. We want our processes to leave zero room for the feature creepers.
Let’s consider the code shown in the following screenshot; it’s playing card hand evaluation test for a Blackjack simulator I’m writing in my spare time (because, incidentally, I’m obsessed with odds analysis).
One of the core requirements of that program is the ability to identify the relative values of different cards. How can I know that it does so correctly? I write a test that uses the program’s function library to produce two different cards — like an Ace of Spades (“As”) and a King of Clubs (“Kc”) — then I have it compare their relative ranks. A successful test should agree with the known fact (i.e. the assertion) that an Ace ranks above a King.
In order to ensure that the program correctly produces a deck of playing cards, I can write a test that instantiates a new Deck and confirms some basic axioms about one. A complete deck should have 52 cards. Specifically, it should contain 13 cards (Two through Nine, Jack, Queen, King, and Ace) for each of the four suits. If that basic test fails, then I know that a fundamental error resides in the code I wrote to define my Deck class, and so I can ignore the subsequent errors until my code passes that test.
It validates your program’s functionality (no more QA!)
Just like type checking helps to validate input, TDD helps to validate functionality. Just like a type declaration will immediately alert about an unfulfilled requirement for a function’s input, a test criteria will immediately alert about an unfulfilled requirement for the program’s functionality.
Traditionally, a dev shop’s QA team validates functionality. The QA folks are the
first second level of defense against problems that cause poor user experiences. Which is to say, they catch the requirements overlooked by the developers or the project spec itself. This layer of overhead in a release pipeline is obviously going to be less efficient than one that effectively catches the errors earlier in the process, leaving a QA team to handle more specialized, less easily automatable testing.
TDD behavior can elevate organizations to the next level.
I’ve become a huge fan of TDD and its relative, behavior-driven development (BDD), because it’s the easy — and hugely effective — answer to our desire to not dedicate time toward documentation while we’d rather be writing code. TDD makes program documentation an integral part of the coding process. Moreover, since the test routines are written for automation, we can hand off that tedious task to a CI/CD pipeline — and justify the build-out of one if we don’t already have one in place. #symbiosis
Just in the last few months, I’ve spoken with several companies who have specifically mentioned TDD/BDD in their upfront pitches for their job openings. Those that strike me as the most driven orgs are frequently among those who ascribe to the test-driven approach. Needless to say that you should be learning TDD by now.
If you don’t yet have a favorite testing framework, I suggest taking a look at Mocha — it’s the one I use in my NodeJS projects, along with Chai for input validation and Istanbul for code coverage analysis and reporting. (Please let me know if you have some other suggestions!)