TDD Beyond Basics : Fakes and Mocks

Objective

  • Learn to be minimal when implementing the production code.

Discussion


Why do we need to be minimal when writing the production code?

Because, the goal of TDD is to end up with a minimal system. Simplicity is the goal.

Why do we aim for a minimal system?

Because it will be maintainable. In a maintainable system, tests will be easy to understand, modify and extend. Keeping your code ready for unexpected changes is about simple design.

Steps


Step 1

In this example, the requirement is that we should be able to assign a role to a given user. Create a user_spec.rb file with the following contents:

describe User do

end

Step 2

Run the spec. We get the error:

uninitialized constant User

Step 3

Add the User class definition to the top of the user_spec.rb as follows:

class User

end

describe User do

end

Step 4

Run the test again. We are green but there are no examples to run yet.

Step 5

Add the first spec:

class User

end

describe User do
  it 'should be in any role assigned to it'
end

Step 6

Run the spec again. You see a pending spec in the output:

Pending:
  User should be in any role assigned to it

Step 7

Change the spec to the following:

class User

end

describe User do
  it 'should be in any role assigned to it' do
    User.new
    user.assign_role('some role')
    user.should be_in_role('some role')
  end
end

Step 8

Run the spec. We get the error:

undefined method 'assign_role' for User

Step 9

Define the assign_role method in User class like this:

class User
  def assign_role

  end
end

describe User do
  it 'should be in any role assigned to it' do
    User.new
    user.assign_role('some role')
    user.should be_in_role('some role')
  end
end

Step 10

Run the spec. We get the error:

wrong number of arguments (1 for 0)

Step 11

Add the argument like this:

class User
  def assign_role(role)

  end
end

describe User do
  it 'should be in any role assigned to it' do
    User.new
    user.assign_role('some role')
    user.should be_in_role('some role')
  end
end

Step 12

Run the spec. We get the error:

undefined_method 'in_role?' for User

Step 13

Add in_role? method to user.rb as follows:

class User
  def assign_role(role)

  end

  def in_role?

  end
end

describe User do
  it 'should be in any role assigned to it' do
    User.new
    user.assign_role('some role')
    user.should be_in_role('some role')
  end
end

Step 14

Run the spec. We get the error:

wrong number of arguments (1 for 0)

Step 15

Add an argument to in_role? method:

class User
  def assign_role(role)

  end

  def in_role?(role)

  end
end

describe User do
  it 'should be in any role assigned to it' do
    User.new
    user.assign_role('some role')
    user.should be_in_role('some role')
  end
end

Step 16

Run the spec. We get the error:

expected in_role?('some role') to return true, got nil

We are now failing for the right reason. Notice that each small step we took was guided by the failure messages given by running the test. We only did just enough to get past the current error message. We were minimal in writing the production code.

Step 17

Change the in_role? implementation like this:

class User
  def assign_role(role)

  end

  def in_role?(role)
    true
  end
end

describe User do
  it 'should be in any role assigned to it' do
    User.new
    user.assign_role('some role')
    user.should be_in_role('some role')
  end
end

Step 18

Run the spec. The example will now pass. We know that this implementation is bogus.

Step 19

We want the specs to force us to write the logic that can handle assigning roles.

class User
  def assign_role(role)

  end

  def in_role?(role)
    true
  end
end

describe User do
  it 'should be in any role assigned to it' do
    User.new
    user.assign_role('some role')
    user.should be_in_role('some role')
  end

  it 'should not be in a role that is not assigned to it' do
    user = User.new

    user.should_not be_in_role('admin')
  end
end

Step 20

Run the spec. We get the error:

expected in_role?('admin') to return false, got true

Step 21

Change the user.rb as follows:

class User
  def assign_role(role)
    @role = role  
  end

  def in_role?(role)
    @role == role  
  end
end

describe User do
  # code remains same as before
end

Step 22

Run the specs. The specs will pass.

Q&A


How do we achieve simplicity?

It's about saying no to future requirements and extra flexibility.

How do you recognize simple design?

Knowledge. Knowledge is insight in problem domain. It develops over time.

How can you achieve simple design?

Gain knowledge. Refactor to reflect new understanding. Code bare minimum. Keep things as simple as possible for as long as possible.

Do we always have to take such small steps?

No. If you are confident that you can get to green quickly you can take bigger steps. If you are taking bigger steps and you end up in red, it is an indicator that you need to take smaller steps.

Exercises


  • Move user.rb to its own class. Make sure all the specs pass.
  • Implement the feature where a user can be in many roles. Remember to write the test first.
  • Download the BDDBasicsI.mov screencast and watch the demo screencast.
  • Watch BDDBasicsII.mov video.
  • Watch BDDBasicsIII.mov video.

Reference


  1. The Test Automation Manifesto - A paper by Gerard Meszaros, Shaun Smith and Jennitta Andrea.


Related Articles


Create your own user feedback survey