Red, Green, Refactor ad infinitum
Red, green, refactor. NOT red, green, red, green, red, green, refactor, refactor, refactor, refact…
(Michael has informed me that he actually got that from somebody’s tweet)
When performing test-driven-development, there aren’t many rules to follow, but the hard part is actually following them. One of the main rules you pretty much always want to live by is, work in small increments. Number two, you always want to do the simplest thing that gets you to green. This can be as simple as just hard-coding a return value in a method so that you’re getting a unit test to pass.
This may seem a rather contrived way to go about things, but that’s how TDD works; you work incrementally, and rather than getting overwhelmed by the magnitude of the whole program you’re trying to write, you’re zeroing in on a small part of it, steadily improving it and assuring that you are progressing because you’re passing your tests.
Let’s take a look at a simple case that helps illustrate these concepts. Let’s say you want to write a program that takes in an array of numbers and performs some statistical calculations on it (e.g. largest number, average, standard deviation, etc.). We’ll start by writing a RSpec test for it:
1
2
3
4
5
6
describe ArrayStatistics do
it 'should output the largest number in an array' do
expect(ArrayStatistics.new([1,2,3]).largest_number).to eql(3)
end
end
We run RSpec, and not surprisingly, we get some “red”; an error (or perhaps, more precisely, a failure):
1
uninitialized constant ArrayStatistics (NameError)
So let’s fix that. We create our array_stats.rb file and do the following (remember to place, in this case, require_relative “array_stats”
in the spec file) :
1
2
class ArrayStatistics
end
Whaaaa? Why in the world are we writing an empty class here? Simple. Because we want to build incrementally and do the simplest thing that works. Run the test again and we should get another error:
1
2
3
4
5
6
7
8
Failures:
1) ArrayStatistics should output the largest number in an array
Failure/Error: expect(ArrayStatistics.new([1,2,3]).largest_number).to eql(3)
ArgumentError:
wrong number of arguments (1 for 0)
#./array_spec.rb:6:in 'initialize'
#./array_spec.rb:6:in 'new'
This time we get a different error, and its because we haven’t initialized our instances of the class ArrayStatistics. Since we are calling the .new
method on our class, we need to initialize
what we are passing (in this case, an array) in as arguments for this instance. We do this by:
1
2
3
4
5
6
7
class ArrayStatistics
initialize(array)
@array = array
end
end
Let’s run our test again and make sure we did something in the right direction. We should get something like this:
1
NoMethodError: undefined method `largest_number'
OK, now we’re starting to get somewhere. We have a NoMethodError
because we’re expecting a largest_number
method, yet it is undefined in our array_stats.rb
. So let’s write the method for it:
1
2
3
4
5
6
7
class ArrayStatistics
...
def largest_number
end
end
So we again run our RSpec and get:
1
2
expected: 3
got: nil
So we got the class defined and ‘initialized’, and we have a method. Now we need our largest_number
method to return 3
, not nil
. Remember, as tempting as it may seem to start thinking up the best way to write this method at this point, don’t do it. It’s crucial, to learning TDD and thus, coding habits, to continue to work in baby steps. Just do the simplest thing that works:
1
2
3
def largest_number
3
end
Run our test once again…
1
2
Finished in 0.00258 seconds
1 example, 0 failures
BOOM! Our test has no errors and is therefore “green”! Start to see the power of TDD, now? See how our tests “drove” the development of our program? Now, at this point, we want to start doing some “refactor”-ing so that our method can return the largest number of any argument we pass to it and not just our particular [1, 2, 3]
one. We might do something like:
1
2
3
def largest_number
@array.max
end
Calling the .max
method on our instance of ArrayStatistics, should, for all intents and purposes, return the largest value of any array we pass to it. Of course, go ahead and run RSpec again to make sure our implementation still passes our test, as it should. After this, we can begin to write test cases for additional functionality in our program (e.g. methods that perform other calculations on the array) and start the whole process again.
And that’s pretty much the basis of TDD. Red, green, refactor, wash, rinse, repeat. Just remember to refactor early and often after passing your tests. You don’t want to put it off until the end when your code is all big and bulky, in which it may turn into a nightmare.
TDD is an awesome technique to use, for many reasons. For me, I feel like I’m actually getting somewhere every time I pass I see green. It may just be one measly test among dozens or more, but it assures me that I’ve done something right, and that’s extremely important. It’s also easier to sniff out code smells early on. I find myself being more efficient and productive, less frustrated, and, quite honestly, having even more fun coding than otherwise.
Except in certain situations, practice TDD whenever you code, because you’ll be much better off with it than without. I’ve talked to a good number of prominent developers in the Boston tech community, and they all live by the principles of TDD; they all say, by continually practicing it, you’ll greatly develop as a programmer and set yourself far apart from those who don’t.
A big shout-out to Mike Denomy, a huge proponent of TDD, who paired with me, and by doing so, introduced me to the world of test-driven-development.