RSpec : let() it be

Première publication : 2009-11-05
Tags : rspecrubytdd

What I’ve found

A few minutes ago, I was watching a great screencast of Corey Haines doing a kata.
I stopped when he was refactoring a few similar assigments. There was something I’ve never seen elsewhere, particularly in the also great RSpec book ; he used the let() method.

Going back and forth a few times, I understood the the method was assigning the result of the given block to an object named after the argument of let().

I googled to see if there was an explanation ; is it a custom helper he wrote, is it built-in RSpec… ? but “RSpec let()” is returning a lot of results, and none was close to the answer I was looking for.

I then looked at the Rdoc for the RSpec gem ; no luck either.

Finally, I open the whole gem in Textmate and looked for “let” as a word.

I’ve found a use of the method in a spec (RSpec is speced with itself, how great is it?) with a similar scenario. I’ve also found the method declaration, and I have to say that it’s pretty self-explanatory :

def let(name, &block)
  define_method name do
    @assignments ||= {}
    @assignments[name] ||= instance_eval(&block)
  end
end

You have to pass a name and a block. It defines a new method (named after what’s in ‘name’). This new method returns the result of the block (whatever it returns) but is memoizes it in a global hash.

You can use the let() method in a describe block, if you want to create a method that is always returning the same value, determined by the result of the passed block. It is something you can do with a traditional before block with a simple variable assignment, but it’d be evaluated each time, though it’s not necessary. A simple variable is also subject to change if it’s not frozen or a constant.
Here, it’s just what it is and only what it needs to be.

What I like

First, I like that some very simple but usefull tools are available. I really sounds like something build after hundreds of times of repeating the same calls… It’s very concise and DRY.

But I also like very much the way the hash is created if it doesn’t already exist, and the value is set only once,even on subsequent calls of the new method.

The dynamically created method could have been defined this way :

define_method name do
  @assignments = {} if @assignments.nil?
  if @assignments[name].nil?
    @assignments[name] = instance_eval(&block)
  else
    @assignments[name]
  end
end

but it’s boringly verbose and repetitive. The first way is way more readable and elegant.

If I have remember just a thing from Corey’s screencast, it’s the existence and the use of this beautiful let() method and how well it is written.

About Corey Haines

Corey is a great guy, one of the few that I really admire.

Amongst the thing that I like about him, he’s doing something that I’d really like to do : he’s traveling all over the USA and is offering to share some working (and fun) times for hospitality and friendship.

You can find about what he does on his profile page.

Thanks Corey !


Update 1 : The entire Kata is available on GitHub. There are 3 branches : the first is the start of the kata, with only the setup and the cucumber expectations. The second and third are implementations.

It’s very useful to read the initial state of the Kata and see where it ends, with all the tests and the implementation code.


Comments

gordon 2011-08-03 14:20:35

merci, Jérémy . Hmmm, wouldn’t it be confusing to have a method name (from let()) be named the same as the instance variable?

Jérémy Lecour 2011-08-03 14:45:50

I don’t think it’s confusing.

As always with some extractions and refactoring, you have to set your own limits beyond which you lose clarity, …

But having the result of a block assigned to a variable, only once, is very interesting in my opinion.

gordon 2011-08-04 00:43:24

But having the result of a block assigned to a variable, only once, is very interesting in my opinion.

–> anything that’s used more than once - make it a function/method . I totally agree with that but coming from a very strong perl background, I guess I freak a little when I see methods and variables sharing the same name (which is a ruby convention). I will need to code and read more ruby :)

thank you :)

Jérémy Lecour 2011-08-03 14:15:49

Hi,

You can find an example in the source code of the Kata : https://github.com/coreyhaines/kata-codebreaker-marker/blob/reference_implementation/spec/marker_spec.rb

@gordon : indeed, in your example, you can call part and it will execute and affect the let block if it is not already set.

gordon 2011-08-03 10:21:22

hi, good post. Very clear description but I do have a few questions as I’m trying to understand this properly. Hope you don’t mind.

How do you call the particular let method that you just declared?

For example:

describe Parts do

  describe "Add new entry" do
    let (:part) { double('Part').as_null_object }

    it ... do
        ...
    end

  end  

end

1) in the example above, how do you call the contents of the double('Part').as_null_object such that a mocked Part object is generated in a before block? would you call :part? I’m trying to understand how this works because typically, in the spec, the :part symbol would also be referring to the @part instance variable in the view file.

2)

 let() {  }

i’m beginning to have a suspicion that the “name” which you feed into a call to let() is to be defined as “the name of the resource which the contents of the block are to be assigned to. For example, the generated of a mocked resource object that is to be used throughout a spec”.