TDD Best Practice : Eliminating Loops in Tests

Objective


The objective of this lesson is to illustrate how to eliminate loops in specs.

Discussion


This article illustrates the Communicate Intent Principle discussed in the xUnit Test Patterns by Gerard Mezaros. This principle is also known as: Higher Level Language, Single Glance Readable. If we have to squint our eyes when we look at the test, then it is harder to understand because we need to infer the big picture from all the details.

The tests must specify and focus on WHAT instead of implementation, the HOW. Loops are implementation details. For an indepth discussion on What vs How, read the What vs How in Test Driven Development article.

Just quickly read the following code for meszaros gem : https://github.com/bparanj/meszaros to see the utility methods that help to eliminate loops in specs. Here is the loop_spec.rb

require 'spec_helper'
require 'meszaros/loop'

module Meszaros
  describe Loop do
    it 'should allow data driven spec : 0' do
      result = []
      Loop.data_driven_spec([]) do |element|
        result << element
      end

      result.should be_empty
    end

    it 'should allow data driven spec : 1' do
      result = []
      Loop.data_driven_spec([4]) do |element|
        result << element
      end

      result.should == [4]
    end

    it 'should allow data driven spec : n' do
      result = []                       
      Loop.data_driven_spec([1,2,3,4]) do |element|
        result << element
      end

      result.should == [1,2,3,4]
    end

    it 'should raise exception when nil is passed as the parameter' do
      expect do
        Loop.data_driven_spec(nil) do |element|
          true.should be_true
        end
      end.to raise_error
    end

    it 'execute code 0 times' do
      result = 0

      Loop.repeat(0) do
        result += 1
      end

      result.should == 0
    end

    it 'execute code once' do
      result = 0

      Loop.repeat(0) do
        result += 1
      end

      result.should == 1
    end

    it 'raises exception for nil parameter in repeat' do
      expect do
        Loop.repeat(nil) do
          true.should be_true
        end
      end.to raise_error
    end

    it 'raises exception for string parameter in repeat' do
      expect do
        Loop.repeat('dumb') do
          true.should be_true
        end
      end.to raise_error
    end

    it 'raises exception for float parameter in repeat' do
      expect do
        Loop.repeat(1.1) do
          true.should be_true
        end
      end.to raise_error
    end

    it 'execute code n number of times' do
      result = 0

      Loop.repeat(3) do
      result += 1
    end

    result.should == 3
   end
end

Here is loop.rb

module Meszaros
  class Loop
    def self.data_driven_spec(container)
      container.each do |element|
        yield element
      end
    end

    def self.repeat(n)
      n.times { yield }     
    end
  end
end

Discussion


From the specs, you can see the cases for looping 0, 1 and n times. We gradually increase the complexity of the tests and extend the solution to a generic case of n. It also documents the behavior for illegal inputs. The developer can see how the API works by reading the specs. Data driven spec and repeat methods are available in meszaros gem.

The WHAT is like a blue print for a house. Blue print does not get buried under implementation details of a house. The HOW is the mechanics of implementation.

alt text

When you see the blue print for a house you can answer questiosn like:

  • What is the size of master bed room?
  • What is the living room?
  • Where is the balcony?

and so on. It does not specify the material used to build the house, paint color, whether the floor will be hardwood floor etc. These are implementation details.

Before


Let's take a look at an example to see how the code would look like when it mixes the WHAT with HOW. The code for before section is stolen from Alex Chaffe's presentation: https://github.com/alexch/test-driven

Matrix Test

class String
  def vowel?
    %w(a e i o u).include?(self)    
  end
end

describe 'Vowel Checker' do
  %w(a e i o u).each do |letter|
    it "#{letter} is a vowel" do
      letter.should be_vowel
    end
  end
end

This mixes the WHAT and HOW. It is not clear. Since the implementation detail buries the intent of the spec. It passes with the message:

$rspec ruby_extensions_spec.rb --color --format doc

Vowel Checker
  a is a vowel
  e is a vowel
  i is a vowel
  o is a vowel
  u is a vowel
Finished in 0.0048 seconds 5 examples, 0 failures

After


How can we communicate intent in the specs? Here is the code after fixing this problem:

Data Driven Spec

class String
  def vowel?
    %w(a e i o u).include?(self)
  end
end

def data_driven_spec(container)
  container.each do |element|
    yield element
  end
end

describe 'Vowel Checker' do
  specify 'a, e, i, o, u are the vowel set' do
    data_driven_spec(%w(a e i o u)) do |letter|
      letter.should be_vowel
    end
  end           
end

Run the specs.

$rspec ruby_extensions_spec.rb --color --format doc

It passes with the following message:

Vowel Checker
  a, e, i, o, u are the vowel set 
Finished in 0.00358 seconds
1 example, 0 failures

This is a specification that focuses only on WHAT. It separates the WHAT from the HOW. The HOW is hidden behind a library call datadrivenspec. The doc string is easily understood without running the program inside your head. Since the spec passed without failing, let's mutate the code like this:

class String
  def vowel?
    !(%w(a e i o u).include?(self))
  end
end

It now fails with the error message:

1) Vowel Checker a, e, i, o, u are the vowel set
    Failure/Error: letter.should be_vowel
      expected vowel? to return true, got false

The error message is also very clear in failure. Now revert back the change. The spec should pass.

Exercises


  • Can you think of another way to mutate the vowel?() method so that the test fails first?
  • Can we use the utility method in the Loop class to square or cube all elements in a given array?
  • Can you use a custom RSpec matcher for the vowel check?


Related Articles


Ace the Technical Interview

  • Easily find the gaps in your knowledge
  • Get customized lessons based on where you are
  • Take consistent action everyday
  • Builtin accountability to keep you on track
  • You will solve bigger problems over time
  • Get the job of your dreams

Take the 30 Day Coding Skills Challenge

Gain confidence to attend the interview

No spam ever. Unsubscribe anytime.