TDD Beyond Basics : Contract Tests

Objectives


  • Learn how to write Contract tests
  • Learn about reliable test.

Discussion


In this lesson you will be introduced to Contract tests. You will learn how to use Contract tests to explicitly document the behavior of an API for invalid inputs. You will also learn about reliable test, a test that fails when it should.

Who should be responsible for handling invalid inputs? Client or the supplier? If we make both client and the supplier code both check for invalid inputs, it is called Defensive Programming.

Why Defensive Programming is not a good idea?

Defensive programming is not a good idea because it results in complexity. The client and the supplier code will be doing the same work and there would be no agreement on who is responsible for the proper handling of the errors. The lack of clear responsibility also means we cannot say whether the bug is in the client or the supplier.

Steps


Step 1

Create week_spec.rb with the following contents:

describe Week do
  it 'returns monday as the first day of the week' do
    day = Week.day('1')

    day.should == :monday
  end

  it 'returns false for out of range numbers' do
    day = Week.day('8')

    day.should be_false
  end
end

Step 2

Create week.rb with the following contents:

class Week
  DAYS = { "1" => :monday, 
           "2" => :tuesday, 
           "3" => :wednesday, 
           "4" => :thursday, 
           "5" => :friday, 
           "6" => :saturday, 
           "7" => :sunday}
  def self.day(n)
    if n.to_i < 8
      DAYS[n]    
    else
     nil
    end
  end
end

Step 3

Run the specs.

$rspec week_spec.rb 
Week
  returns monday as the first day of the week
  returns false for out of range numbers
Finished in 0.06081 seconds 
2 examples, 0 failures

Step 4

Change the implementation of the day method like this:

def self.day(n)
  DAYS[n]
end

Step 4

Run the specs. It will still pass. Because, in Ruby, accessing a hash that does not have the given key will return nil, which evaluates to fase. Here the implementation is explicit in order to illustrate a problem.

Step 5

Change the day method implementation in week_spec.rb like this:

def self.day(n)
  if n.to_i < 8
    DAYS[n]
  else
    ''
  end
end

Step 6

Run the specs again.

Failures:
1) Week returns false for numbers that does not correspond to a week day 
   Failure/Error: day.should be_false
    expected: false value
      got: ""
     ./week_spec.rb:27:in ‘block (2 levels) in <top (required)>’
Finished in 0.00488 seconds
2 examples, 1 failure
Failed examples:
rspec ./week_spec.rb:24
Week returns false for out of range numbers

Test fails.

Discussion


Test breaks when the production code changes the return value from nil to blank string. Test fails when it should. This is good. If the clients use a conditional statement to check the true/false value, they will be protected by this failing test, since the defect is localized. Violating the contract between the client and library results in a failing test. We have to fix this problem so that the existing clients using our library does not break.

Step 7

Let's revert back the implementation to working version. Since clients are dependent on the returned false value of nil.

class Week
  DAYS = { "1" => :monday, 
           "2" => :tuesday, 
           "3" => :wednesday, 
           "4" => :thursday, 
           "5" => :friday, 
           "6" => :saturday, 
           "7" => :sunday}
  def self.day(n)
    if n.to_i < 8
      DAYS[n]    
    else
     nil
    end
  end
end

The specs remain the same. Here is the code:

describe Week do
  it 'returns monday as the first day of the week' do
    day = Week.day('1')

    day.should == :monday
  end

  it 'returns false for out of range numbers' do
    day = Week.day('8')

    day.should be_false
  end
end

If you are versioning your API, then you could make changes that can break your clients. In this case, you would deprecate your old API and give sufficient time for the clients to migrate to your newer version.

Step 8

Let's add contract tests that explicitly documents the behavior of the API for invalid inputs. Hash#fetch method throws exception for cases that was implicit in the code.

Here is the code for week.rb:

class Week
  DAYS = { "1" => :monday, 
           "2" => :tuesday, 
           "3" => :wednesday, 
           "4" => :thursday, 
           "5" => :friday, 
           "6" => :saturday, 
           "7" => :sunday}
  def self.day(n)
    if n.to_i < 8
      DAYS[n]    
    else
     nil
    end
  end

  def self.end(n)
    if n.to_i < 5
      raise "The given number is not a weekend"
    else
      DAYS.fetch(n)
    end
  end
end

Here is the specs:

describe Week do
  # existing contract test for week day
  it 'returns false for out of range numbers' do
    day = Week.day('8')

    day.should be_false
  end

  # new contract test
  it 'throws exception for numbers that is not weekend' do
    expect do
      week_end = Week.end('4')
    end.to raise_error
  end

  it 'throws exception for numbers that is out of range' do
    expect do
      week_end = Week.end('40')
    end.to raise_error
  end
end

Step 9

Run all specs.

$rspec week_spec.rb
Week
  returns monday as the first day of the week
  returns false for out of range numbers
  throws exception for numbers that is not weekend
  throws exception for numbers that is out of range
Finished in 0.00743 seconds 4 examples, 0 failures  

Discussion


Apply Bertrand Meyer's guideline when deciding about exceptions: When a contract is broken by either client or supplier, throw an exception. This helps us to determine whether the bug is in the client or the supplier code. In our case, the contract is that as long as the client provides the proper input, the supplier will return the corresponding symbol for the week.

A program must be able to deal with exceptions. A good design rule is to list explicitly the situations
that may cause a program to break down.
---- Jorgen Knudsen from Object Design : Roles, Responsibilities and Collaborations

Explicitly document the behavior of your API by writing contract specs. This will help other developers understand and use your library as intended.

Summary


In this lesson you learned about contract tests and why it is important to include them in your specs. You also learned about Bertrand Meyer's guideline about throwing exceptions.


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.