Red, Green, Refactor ad infinitum

Red, green, refactor. NOT red, green, red, green, red, green, refactor, refactor, refactor, refact…

-Michael Denomy Cyrus Innovation

(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:

array_stats_spec.rb
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):

terminal
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) :

array_stats.rb
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:

terminal
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:

array_stats.rb
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:

terminal
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:

array_stats.rb
1
2
3
4
5
6
7
class ArrayStatistics

...

  def largest_number
  end
end
But wait, wait, Teach, Teach, isn’t that just an empty method that doesn’t return anything? Yes, but remember, we want to work in tiny, microscopic steps. This way we know if we’re heading in the right direction, and if we’re not, we know what we just did wrong and can easily backtrack to it. Imagine working busily coding away for a good hour or so. If you run your test and hit an error, or a bunch of errors, it could get really frustrating to figure what and where things went wrong.

So we again run our RSpec and get:

terminal
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:

array_stats.rb
1
2
3
def largest_number
  3
end

Run our test once again…

terminal
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:

array_stats.rb
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.