Brian TakitaBrian Takita
RR 0.6.2 Released
edit Posted by Brian Takita on Tuesday October 28, 2008 at 06:07AM

This release features a sexier Double definition block syntax using instance_eval:

mock(User) do
  find(1) {user_1}
  find(2) {user_2}
end

RR also maintains its non-instance_evaled Double definition block syntax, if the block has an arity of 1:

mock(User) do |expect|
  expect.find(1) {user_1}
  expect.find(2) {user_2}
end

The instance_eval syntax is very useful in the context of Double Definition chaining:

mock.proxy(User).find(1) do
  mock.proxy! do
    blogs do
      mock.proxy!.find_by_id(2) do |actual_blog|
        actual_blog.name.should == "My ranty blog"
        actual_blog
      end
    end

    fans do
      mock.proxy!.thank_everybody
    end

    foes do
      mock.proxy!.ask_for_forgiveness
    end
  end
end

user = User.find(1)
user.blogs.find_by_id(2) # My ranty blog
user.fans.thank_everybody
user.foes.ask_for_forgiveness

Of course the previous example is a complicated case of interaction testing, and a simpler state-based and/or hybrid approach may be better, but it demonstrates how using instance_eval can promote readability.

Here is the non-instance_eval solution:

mock.proxy(User).find(1) do
  mock.proxy! do |expect|
    expect.blogs do
      mock.proxy!.find_by_id(2) do |actual_blog|
        actual_blog.name.should == "My ranty blog"
        actual_blog
      end
    end

    expect.fans do
      mock.proxy!.thank_everybody
    end

    expect.foes do
      mock.proxy!.ask_for_forgiveness
    end
  end
end

instance_eval Controversy

Ola Bini warned against overusing instance_eval. While instance_eval adds beauty, it can also make things more difficult to extend and debug.

In RR, the DoubleDefinitionCreatorProxy (the object that is instance_evaled when defining Doubles using blocks) uses the blank slate pattern, so arbitrary method names can be passed in to define the Double. The Blank Slate implies that the DoubleDefinitionCreatorProxy will not be extended with methods. James Earl Gray explains the pattern that I used for RR.

spec/test helper methods will not be usable within the instance_evaled blocks because the intent of the DoubleDefinitionCreatorProxy object is incompatible with the instance_eval with delegation pattern. DoubleDefinitionCreatorProxy already uses method_missing to create DoubleDefinitions. If you wish to use spec/test helper methods, you will need to memoize it to a variable and use lexical scoping.

Now, one may want to define a helper method in the test/spec that is returned when the Double is invoked.

describe User do
  describe "#fans.thank_everybody" do
    it "thanks all of my lovely fans" do
      user = User.new
      memoized_my_lovely_fans = my_lovely_fans
      mock(user) {fans {memoized_my_lovely_fans}}
      memoized_my_lovely_fans.each {|fan| mock(fan).thank_you}

      user.fans.thank_everybody
    end

    def my_lovely_fans
      [Fan.new, Fan.new]
    end
  end
end

While I think this will be a good addition to RR, I recognize that adding the instance_eval has the possibility of making RR less usable. I'll pay close attention to see how this pans out and am willing to remove it if there are major issues.

Comments

  1. Brian McManus Brian McManus on February 12, 2009 at 08:49PM

    I was not sure where else to go with this question since the mailing list mentioned on github/rubyforge has never been posted to and nobody in #rubyonrails or #rspec that knows RR responded...

    The docs for RR make it sound very much like:

    mock(my_mock_object = Object.new).hello
    

    and

    my_mock_object = mock!.hello
    

    are the same. Specifically from the part where you mention "Pure Mock Objects" in the github readme.

    Can you help me decipher, then, why the following are not equivalent? (This is from an attempt to switch restful_auth to rr instead of rspec mocks)

              @ccookies = mock!.[](:auth_token){token_value}
              #mock(@ccookies = Object.new).[](:auth_token){token_value}
              stub(@ccookies).delete(:auth_token){true}
              stub(@ccookies).[]={true}
              @ccookies.methods.each {|method| puts "RSPEC: #{method}" if method.to_s =~ /__rr__/}
              stub(controller).cookies{@ccookies}
    

    Note that I have a similar loop looking for rr methods in authenticated_system.rb prefixed with AUTH: when it attempts to read the cookies var. The problem seems to be with the first two lines. If I run this test as it is above, nothing works and I get output like:

    RSPEC: rr[]= RSPEC: rrdelete AUTH: rr[]

    If, however, I comment out the first line above and uncomment the 2nd line things make more sense and I get:

    RSPEC: rr[]= RSPEC: rr[] RSPEC: rrdelete AUTH: rr[]= AUTH: rr[] AUTH: rrdelete

    Why does the object returned by stubbed controller cookies method not see the additionally stubbed delete and []= methods when I do things one way but it sees all 3 stubbed methods the other way?

  2. Betty Betty on March 18, 2009 at 05:06AM

    I recently came across your blog and have been reading along. I thought I would leave my first comment. I don't know what to say except that I have enjoyed reading. Nice blog. I will keep visiting this blog very often.

    Betty

    http://laptopprocessor.info