TDD Really Works
As I learn more about Rails and become more involved in the Ruby community, one of the universal truths that I am coming to realize is that we care greatly about writing tests. So I decided to force myself to do more test driven development, both at work and in my personal projects. I’ve been digging into Rspec and the various gems that go along with that including Capybara, Factory Girl and Faker (I might do a post about these later).
Since I am just starting out with a serious TDD practice, it can be a little hard to find the motivation to stick with it. I know for me, my initial reaction was to think, “Why am I wasting my time writing tests, I would rather just use the time to actually write the code.” A couple of weeks ago; however, I had an experience that started to make me realize the real value of TDD.
In an attempt to strengthen my Ruby chops and get better at code review (something I really need), I’ve been doing coding exercises from exercism.io, a community of programmers who complete challenges and then go through other solutions and offer constructive feedback. One of the exercises is called Raindrops, and is a variation on the Fizzbuzz problem. In the Raindrops problem you write a program that converts a number to a string, the contents of which depends on the number’s factors. If the number contains 3 as a factor, output ‘Pling’, if the number contains 5 as a factor, output ‘Plang’, if the number contains 7 as a factor, output ‘Plong’, and if the number does not contain 3, 5, or 7 as a factor, just pass the number’s digits straight through. For example:
Raindrops.convert 28 # => Plong Raindrops.convert 30 # => PlingPlang Raindrops.convert 34 # => 34
When you download exercism problems, they come with a test suite pre-written and
the first time I tackled this challenge, I really struggled. My approach was to
just write the code, and then test against all the test cases at once. For some reason, I
could not get all the tests to pass. In some instances the code outputted the
correct conversion, but other times it would just output
nil and I had no idea
why. I decided to just scrap it and start from scratch.
This time, I deliberately went one test case at a time writing out a solution that solved that case. Here is the code that came from that process.
class Raindrops def self.convert(number) response = "" if (number % 3 == 0) response << "Pling" end if (number % 5 == 0) response << "Plang" end if (number % 7 == 0) response << "Plong" end if response.empty? return number.to_s else return response end end end
From a code elegance perspective, its not ideal. Its a little clunky, but all the test passed (!!) and you can sort of tell by the structure of the code that I went one test case at a time. The first test case was for a number divisible by 3, so that conditional went first, then 5 and so on as more complicated tests came up.
I submitted that solution and got some great feedback from other programmers which led me to my final solution.
class Raindrops def self.convert(number) sound = "" sound << "Pling" if (number % 3).zero? sound << "Plang" if (number % 5).zero? sound << "Plong" if (number % 7).zero? sound.empty? ? number.to_s : sound end end
That is much better. My path to this solution was classic “Red, Green, Refactor” and it was both satisfying and it gave me a practical example of how TDD is useful.