TDD in 10 minutes

Test Driven Development can seem like a scary topic for most new developers but the concepts are actually quite simple. Once you get the hang of TDD, the benefits become clear. Your code will tend to be simpler, cleaner and to the point. With TDD, we only write the code we need to write and avoid adding unnecessary fluff. Making changes to your code will no longer be a scary prospect. You have tests to verify that your changes wont break existing code. If something does go wrong, you will know exactly where the issue is.

The complete code for this blog post can be found here on Github.

Goal

Our goal for this exercise is to create a Calculator class that has a single method. That method will return the sum of two values. This is our only strict requirements. How we implement this method is up to us. This is often how we will receive work from non-technical people. We need to make decisions on how our methods work and create tests to validate these assumptions work.

Start with tests

TDD states we should create our tests first before writing our actual code. We will be following this approach in the following example. We will also be creating our own mini 'testing framework'. This is a bit of an over statement since it will contain a single method that tests equality. The idea is the same though, a full fledged testing framework will just provide more options to validate our test.

Create a class named CalculatorTest with a single private method named equal?. This method will test the equality of two values. If they are equal it will print PASS to the console. If they are not equal it will print FAIL.


class CalculatorTest
  private
  # Test if two values are equal
  def equal?(a, b)
    if a == b
      puts 'PASS'
    else
      puts 'FAIL'
    end
  end
end

Not to complex. Now let's create our first test. In the goal section, we stated our goal: create a class with a method that returns the sum of two values. Our first test should verify this requirement.


class CalculatorTest
  def should_add_two_values()
    # This is the value we expect to be return by the add method
    expected = 5

    calc = Calculator.new
    # Call the add method with two values that equal 5
    result = calc.add(3, 2) 
    
    # Check if result equals the expected value
    equal?(result, expected) 
  end

  private
  # Test if two values are equal
  def equal?(a, b)
    if a == b
      puts 'PASS'
    else
      puts 'FAILS'
    end
  end
end
tests = CalculatorTest.new
tests.should_add_two_values

The first line of our test sets our expected value to 5. There is nothing special about this value. I chose it at random but we could have put any number here. Then we create and instance of our future Calculator class and call the add() method passing two values that should equal our expected value of 5. We have begun to define what our Calculator class will look like. As we write more tests, we will make more decisions about the composition of our code. The tests will become a checklist of features our code must complete.

Finally, we check to see if the result of the add method is equal to the expected value. If you run this code as is, it will fail because we haven't defined our Calculator class. Let's define that class now.

Implement our code


class Calculator
  def add(a, b)
    a + b # The most simple way to make our test pass
  end
end

Our new Calculator class consists of a single method, add, that takes two values and returns the sum of those values. This is the simplest solution we could implement to get our test to pass. This concept is important when following TDD. Only ever write the code required to make your test pass. This will help us avoid the fluff we talked about before and avoid having code that isn't tested.

More Tests

If we run our code we will see PASS displayed in our terminal. Now that we have one test completed, we should think about other ways someone might try to use our code. What if someone tried to pass two nil values to our method? Let's create a test to see what might happen.


# This method should be added to our CalculatorTest class
def should_add_two_nil_values()
  # We need to decide what we expect the result of nil + nil is
  expected = 0

  calc = Calculator.new
  result = calc.add(nil, nil)
    
  equal?(result, expected)
end 
...
tests.should_add_two_nil_values

The second test is a lot like the first test. We changed the expected value to 0 and the two values passed to the add method to nil. For the expected value, I decided I wanted the add method to return 0 if we pass it two nil values but you might decide you want your method to return nil. These type of choices might be up to you or you may need to ask the person who requested the feature. It's possible they might not have even thought about it. You just need to make sure your test validates whatever assumptions are made.

If you run our test, we would get the following error:

undefined method `+' for nil:NilClass (NoMethodError)

We've discovered it's not possible to add to nil values together. This means we need to change our method to handle nil values. A simple way to do this is to cast our parameters, no mater what type they are, to integers. For NilClass, this will result in the value 0. So that means instead of nil + nil we get 0 + 0 which equals 0.


class Calculator
  def add(a, b)
    a.to_i + b.to_i
  end
end

If we rerun our tests, all should pass. We should continue this line of thinking until we are satisfied we have thought of all possible ways our code might be used. If we want our code to be robust, then we need to think about how future developers might use it. By doing this, we can safely use our code knowing that if someone else makes changes, our tests will act as a warning alarm.

Moving Forward

TTD using other frameworks like Rspec isn't much different then what we've done hear. The concepts are the same, we just have more ways to write our tests. Try not to get overwhelmed by the number of features available in your testing framework. It's more important we write tests then to find the best way to write them. Just like our production code, tests can be refactored over time.