Test-Driven Development - What and why?
In the process of describing my career change to non-developer friends, I found myself explaining time and again what exactly I learned at Makers Academy, and how it was possible to make a complete career transition in only twelve weeks. In truth, it’s impossible for a 12-week course to replace a 4-year university degree. However, at the same time it is also true that in most CS programs, much of the content is theoretical, and relatively little time is dedicated to learning how to write code – and even less is spent on learning how to implement industry best practices. Coding bootcamps are not intended to replace 4-year degrees – rather, they address a gap in the market for junior developers who are ready to write code and start adding business value right away.
One of the key practices that top software development companies value is an understanding of Test-Driven Development (TDD). TDD is the practice of writing a test which represents an expected behavior, running the test, making sure that it fails initially, and then writing just enough code to make that particular test pass. There are several main goals of TDD, from what I understand and my experiences. I’ve tried to explain them as non-technically as I can…
Plan what to write before you write it.
In exactly the same way that you would outline an essay before writing it, setting up a test creates direction to your work process. When you get past the first learning curve of understanding method calls and their returns – the objective, logical parts of writing code – you will encounter more open-ended, subjective questions such as “What is the best way to make this object perform this behavior?” Planning with pen-and-paper is vitally important for the initial design, but once you have the pseudo-code, the next step is to use real code to capture the intended behavior. The process of writing out a test is incredibly useful as a mental anchoring mechanism. Oftentimes when I’m messing around in irb or pry, I lose focus because I accidentally discover something really cool that I want to implement. But knowing that I still have that test that needs to pass helps to mentally re-orient to the task at hand.
Don’t write any more code than you need.
Having extraneous code is not only confusing for other people that have to work on a codebase, it can be harmful because unnecessary methods can cause unexpected behavior that interferes with the core function of an application. It also violates a rule of thumb in agile software development: “You Ain’t Gonna Need It”, which is self-explanatory. It is always possible to add additional functionality later on, when that business need arises.
Make code more human-readable without turning the code into a comment wasteland.
Ruby happens to be a language where the methods have fairly intuitive names, but this is not always the case. A common tactic is to add tons of comments to the codebase to explain what each method does. However, a well-written test framework also achieves this in a less redundant, more functional way. Well-designed test suites will even try to mirror English syntax.
TDD in Practice
TDD in its initial steps feels like it involves a lot of “faking” methods. For example, if we wanted to write a method that intakes a number and then returns the string “Fizz” if the number is divisible by 3, and our first test looked something like:
require 'fizzbuzz' describe Fizzbuzz do it 'should say "Fizz" on a number divisible by 3' do expect(subject.says(3)).to eq "Fizz" end end
To make this pass, we don’t need to write all of the back-end math logic or any type of boolean-to-string conversion just yet. We literally just need an object, that has a method called “says”, that takes one argument, and returns the string “Fizz”.
class Fizzbuzz def says(number) "Fizz" end end
A few other things are happening here – if you’re familiar with RSpec and Ruby, using ‘subject’ as the method-caller is a bit of RSpec magic that conjures up an instance of the class in the main “describe” block. If you’ve never used RSpec and/or are new to Ruby, don’t worry. The main takeaway is that I haven’t written a lick of logic yet. We are just using brute force for now to make the test pass.
Obviously, this method designed in this way won’t hold up for very long, when we start adding more features of Fizzbuzz. When we add the specification that Fizzbuzz also must not say “Fizz” if a number is not divisible by 3, we could evolve our tests like so:
require 'fizzbuzz' describe Fizzbuzz do it 'should say "Fizz" on a number divisible by 3' do expect(subject.says(3)).to eq "Fizz" end it 'should not say "Fizz" on a number not divisible by 3' do expect(subject.says(4)).not_to eq "Fizz" end end
The code in its current state will fail the second test, so we could add something like:
class Fizzbuzz def says(number) "Fizz" if number == 3 end end
This will return nil if the number is anything other than 3. Again, not a very robust fix, but enough to make the second test pass. This is the general idea: using TDD will often feel like you’re faking things until the last possible moment. When we add more requirements like “should say ‘Fizz’ on any number divisible by 3”, “should say ‘Buzz’ on any number divisible by 5”, and so on, we will evolve our code to follow. Fizzbuzz is a very naive example, but in the bigger picture, writing code to meet only the stated requirements is tremendously important in business settings. Why spend time and energy to deliver a feature that your client has not asked for?
(For more examples of TDD, see my PHPUnit blog post below.)
Learning to Test-Drive
During my first few weeks at Makers, I often felt annoyed and constrained by writing tests before writing code. I reluctantly fell into the rhythm of using TDD thanks to a few pairing partners early on who were very insistent on following the process. I wasn’t really completely sold on TDD until after I graduated, and I wanted to brush up on my basic OOD skills by doing some katas on codewars. For some of the higher-level katas that involved multiple steps, I found it difficult to keep track of what was happening as objects were being passed around to different methods. So, what did I do? I fired up terminal and wrote my own test suite to break down those katas with RSpec unit testing!
The biggest validation of TDD for me actually occurred during a pair-programming interview. I chose to build a Sinatra application test-driven with RSpec, Capybara, and Cucumber. When all of the model logic was built and I was ready for the “grand finale” of clicking through the pages of the app, Sinatra unexpectedly combusted. Neither the interviewer nor I could figure out why that particular error was being thrown. I actually believe that at the time, the latest Sinatra release had a slight error that caused it to not play well with Mailgun, a gem that my application relied on, but I’m not 100% sure. However… because I had acceptance tests, everything worked perfectly fine in Cucumber and Capybara. I wound up getting invited back for another round because thanks to my test suite, I could prove that my code worked. Had I not set up acceptance tests, I would have been ROYALLY screwed, and I’m not sure I would have been invited back.
So that, among other things, is the beauty of TDD. TDD allows you to “see” what is happening with your code. It feels very scientific. It actually reminds me a lot of my high school biology classes, where we learned about DNA and various cell microstructures that are unobservable to the human eye even through a microscope, so we need experimental devices like gel synthesis to make DNA properties observable and measurable.
I’ve been having a lot of conversations lately with friends who are interested in learning to code. I would say to them, and to anyone else who is shopping around for a good coding bootcamp: Don’t bother if they don’t teach TDD from Day One. It is true that there is market demand for software developers, but there is greatest demand for developers who are up-to-date with best practices and who rigorously test-drive their code. Knowing TDD is one of the best ways to distinguish yourself from the competition, including those who have 4-year degrees.