Monday, June 24, 2013

Ruby: Mocking with Surrogate

Many people, most notably Martin Fowler, have written about mocking so I won't go into what it is and when to use it. (That is not to say I know the answer to both questions. It is something that I still need to research further. Maybe another post?) Eric Meyer has also written two posts on Surrogate at 8th Light blog (1, 2) but I don't think it hurts to add one more. This post assume you:
  • know Ruby
  • know Rspec
  • want another mocking mechanism for Ruby with Rspec
Let's say you have a game class that you are trying to test: 
class Game

  def initialize(rules)
    @rules = rules
  end

  def over?
    return true if @rules.winner
    return true if @rules.tied?
  end
end

This is what the Rules class looks like:
class Rules
  def initialize(board)
    @board = board
  end

  def tied?
    !winner && @board.filled?
  end

  def winner
    @board.unique_marked_values.detect {|p| win?(p)}
  end

  private
  def win?(player)
    square_sets.any? do |squares|
      squares.all? {|square| square == player}
    end
  end

  def square_sets
    @board.rows + @board.columns + @board.diagonals
  end
end
Note there are 4 methods in Rules, but only two are public, tied? and winner. Assuming you want to mock Rules because it is too complicated to set up a tied or winner situation.
  1. Create a mock folder in spec
  2. Create a mock file (you use the same name as your class file, in my case, rules.rb)
  3. In your mock file
    • Require surrogate
    • Endow the mock class
    • Define the class methods
      #game/spec/mock/rules.rb
      require 'surrogate/rspec'
      
      class MockRules
      
        Surrogate.endow(self)
      
        define(:tied?) do
          false
        end
      
        define(:winner)
      
      end
      
  4. Test if your mock is substitutable for the original class
    #game/spec/game/rules_spec.rb
    require 'game/game'
    require 'game/rules'
    require 'mock/rules'
    
    describe Game do
      it "checks if MockRules is substitutable for rules" do
        MockRules.should be_substitutable_for(Rules)
      end
    end
    
  5. Use the mock class
Let's go into a little more detail for step 3, 4, and 5.

3. Define your class method
There are three way to define your methods:
  1. One line with return type
  2.   define(:tied?) {false}
    
  3. Three lines with return type
  4.   define(:tied?) do
        false
      end
    
  5. One line with default return
  6.   define(:tied?)
    
    What this method really returns is "nil" and since nil evaluate to false as well, you can think of it as false.
If your method has a parameter, like this:
def winner(board)
   .....
end
then in your mock class, you can define the method as follow:
  define(:winner) do |value|
    value
  end
or
  define(:winner) {|value| value}

4. Test if the mock class is substitutable 
This step is important because it ensure your class has the same methods/interface as the class you are mocking. This means if I add the method def name to Rules, when I run rspec, it will fail. Or if I decide to remove def winner in Rules, running the test will fail. Again, this restrict your MockRules to have only methods defined by Rules.

5. Use the mock class
Let's say you want to test that the game is over when there is a tied. Again, this is the game over method
def over?
    return true if @rules.winner
    return true if @rules.tied?
    return false
end
For this test, you want to return true when rules.tied? is called and check to see if rules.winner is called. Here are the steps:
  1. Create a mock class with method you want to override
  2. Create the class under test
  3. Set the mock to game
  4. Call the method under test
  5. Verify expected behaviors
    • Verify over? is true
    • Verify winner was called
  it "is over when the game is tied" do
    rules = MockRules.factory(tied?: true)  #1. Create mock class with specified behavior
    game = Game.new(rules)                  #2. Create a game class 3. "Set" mock to game
    game.should be_over                     #4. Call method under test and 5. Verify expected behavior 
    rules.was asked_for :winner             #5. Verify expected behavior
  end
Note, some say that you should only test for one behavior in each test. I have not figure out where I stand on this argument, but this post is about using surrogate so it's easier for you to read if I put everything under one test. Surrogate can do a lot more. This is just a basic overview on how to use it.

Resource

0 comments:

Post a Comment