Aeonaut
Aeonaut

Reputation: 1267

What is the connection between variable and symbol in Ruby Rake?

I'm new to Ruby programming, but I think I more or less get the point of a symbol (an immutable value, takes up less space, sort of an enum, etc.) Here's what's bothering me -- from a Rake test in the Hartl Ruby on Rails tutorial:

describe "something" do
  let(:user) { FactoryGirl.create(:user) } 
  before { sign_in user }
...
end

It makes sense to me to call FactoryGirl.create(:user) -- here we are passing a sort of flag (:user) to FactoryGirl to tell it what kind of object to make (or, more accurately, what variables to set, since it only creates User objects). But in let(user), user is a variable, not a symbol (or at least it should be). I can only assume that the let method is secretly swapping out the symbol :user for a variable of the same name -- but why? Wouldn't let(user) be a much more intuitive syntax? What am I missing here?

Also, are there other places where this pattern occurs in Ruby? So far I've only seen it in rake tests.

Upvotes: 0

Views: 462

Answers (2)

Dave S.
Dave S.

Reputation: 6419

A few minor corrections / clarifications:

  1. This is a 'rspec' test, not a rake test. Rake is an automation tool (akin the 'make') but rspec is a testing tool.
  2. FactoryGirl can create many different kinds of objects. If you define multiple factories, the symbol passed to #create tells it which factory to use. It does not tell it what variable to set in your program.
  3. The let() call defines a helper method given by the name of the supplied symbol, e.g. :user. The method returns the value determined by the block passed to it, e.g. the user object that FactoryGirl will create.

I've glossed over some of the details of the let() stuff. The real docs are here:

https://www.relishapp.com/rspec/rspec-core/v/2-6/docs/helper-methods/let-and-let

In my personal experience this particular pattern is nc, but unit tests tend to be different from regular code since the idea with them is to exercise the code, not to solve an actual problem. To do that you tend to want to vary one thing at a time so things can get repetitive - but ruby provides lots of tools to cut down on typing and still keep things readable.

EDIT: See comment for examples where this pattern is used. It was right under my nose... :)

Upvotes: 0

Rodrigo Kochenburger
Rodrigo Kochenburger

Reputation: 376

Like Dave already mentioned, the code provided is not a rake task but rather a RSpec specification.

But let me focus on the real problem here.

Symbols are a little hard to grap at first if you never have contact with any other language that have a similar feature. Some language call it Atom.

http://en.wikipedia.org/wiki/Symbol_(programming)

The idea behing a symbol is to provide a primitive type that is humanly readable but computationally cheap.

In Ruby, when the compiler/interpreter sees a symbol, it creates an object of type Symbol and store it in memory. In ruby, symbols are singletons so any other use of the same symbol returns the exact same object, which makes it really cheap in terms of space and also really cheap to compare, since you can just compare the memory address instead of the content.

For example, if you compare two symbols, like this:

:foo == :foo

You're pretty much comparing the same object, meaning only the memory addresses needs to be compared.

Now, when you compare two strings:

"foo" == "foo"

It creates two instances of String with the same content, and needs to compare each byte of the string to make sure they are equal.

This property makes Symbols really good for identifiers or keys in a hash.

Now, to RSpec.

Let's take the following example:

describe Authenticator do
  let(:user) { Factory.create(:user) )

  it "authenticate" do
    auth_user = subject.authenticate(user.login, user.password)
    auth_user.should == user
  end
end

Factory.create takes a symbol as a identifier of the factory to use. You need to define the factories yourself, so really its just a name. You could use a string but it's cheaper and best practice to use symbols but to be honest, it wouldn't make much difference unless you're calling Factory.create million of times.

The let is not defining a variable, it's actually defining a method that does a few things:

  1. The first time the method is called inside a specification (it block), it will execute the block, cache the result and return it
  2. Any other call inside the same specification (it block) will just return the cached result
  3. After the specification is done, it deletes the cached result so that it will re-revaluate on the next call, on the next specification

This allow for lazily creating the objects, only when needed, and to allow restricting any state change to the current specification.

So, bottom line is: RSpec is using the symbol as a identifier for a method name that will be generated to abstract away certain things, making your life easier. RSpec is nothing but a BDD domain-specific-language that uses meta-programming to build a test-suite.

The same behavior could be achieved with the following test case:

class AuthenticatorTest < Test::Unit::TestCase
  def user
    return @user if @user
    @user = Factory.create(:user)
  end

  def subject
    return @subject if @subject
    @subject = Authenticator.new
  end

  def teardown
    @subject = nil
    @user = nil
  end

  def test_authenticate
    auth_user = subject.authenticate(user.login, user.password)
    assert_equal auth_user, user
  end
end

Note that you probably shouldn't write a test case like this, but it (roughly) illustrates what RSpec does.

I hope that helped.

Upvotes: 2

Related Questions