Annoying RSpec: Dynamic Scope

At SoundCloud, we use a fair chunk of different languages, but most of our current code base uses Ruby; usually web apps built with Rails/Sinatra and workers using EventMachine.

Ruby has a very mature test framework called RSpec. I really like it, even used it through JRuby to test Java code. There are a few things with the way some people use the framework which really puts me off, though. Let's discuss some of them in a series of posts.

Scoping Rules

A while back I wrote on why reading classic computer science and software engineering books is important. One theme you are going to see over and over on these books is the issue of scoping.

There are many different ways to implement scoping in a system, the two most common strategies are lexical and dynamic. Summarising a very long discussion, scope is where the runtime and/or compiler should look to check if something you are trying to use (e.g. a variable identifier) is valid (i.e. it is bound).

Ruby has lexical scope, which pretty much means that we must declare variables somewhere before using them. In the code below, user is not a valid variable, as it isn't bound to anything:

describe Group do
 it 'adds user to the group' do
   group = Group.new
   group << user
   group.members.should == [user]
 end
end

We can fix this easibly by doing:

describe Group do
  it 'adds user to the group' do
    user = User.new
    group = Group.new
    group << user

    group.should include(user)
    group.members.should have(1).item
  end
end

RSpec's DSL has a useful construct called let which lets you define variables you want to use in more than one example —just like one would use an instance variable in a non-DSL framework like JUnit. We can use it like this:

describe Group do
  let(:user) { User.new }

  it 'adds user to the group' do
    group = Group.new
    group << user

    group.members.should == [user]
  end

  it 'removes user from group' do
    group = Group.new
    group << user

    group.remove!(user)

    group.members.should == []
  end
end

Some other languages provide dynamic scope, the classic examples are from the LISP languages, including Common Lisp (but not Scheme). Here is an example in Emacs Lisp (ELisp):


(defun is-expected? (other-thing)
  (eql the-thing other-thing))

(set 'the-thing 999)
(is-expected? 666) ;; returns false
(is-expected? 999) ;; returns true
the-thing ;; returns 999


(let ((the-thing 666))
  (is-expected? 666)  ;; returns true
  (is-expected? 999)) ;; returns false
the-thing ;; returns 999

(let ((the-thing 0))
  (is-expected? 666)  ;; returns false
  (is-expected? 999)) ;; returns false
the-thing ;; returns 999

(defun multiply-and-compare (what)
  (is-expected? (* what 100)))

(let ((the-thing 100))
  (multiply-and-compare 1)) ;; returns true
the-thing ;; returns 999

The value of the-thing depends on when it was invoked. This is an extreme case of temporal coupling, very similar to the problems caused by using global variables.

Nevertheless, dynamic scope can be really useful. It is often a great tool when you need to implement embedded DSLs. To use it, though, one has to adopt many conventions and tricks to avoid losing sanity; and as Doug Hoyte discusses in his awesome Let Over Lambda book:

Dynamic scoping used to be a defining feature of lisp but has, since COMMON LISP, been almost completely replaced by lexical scope. Since lexical scoping enables things like lexical closures (which we examine shortly), as well as more effective compiler optimisations, the superseding of dynamic scope is mostly seen as a good thing. However, the designers of COMMON LISP have left us a very transparent window into the world of dynamic scoping, now acknowledged for what it really is: special.

And even Emacs Lisp, in its last version, has lexical scoping.

Dynamic Scoping in RSpec

So how is this related to RSpec usage? Well, turns out that RSpec's embedded DSL provides an equivalent of dynamic binding, and pretty much every Ruby project I was part of uses this "feature".

Here is an example:

# event_consumer_examples.rb
shared_examples_for 'event consumer' do
  it 'raises error if asked to consume invalid event' do
    expect { subject.consume(invalid_event) }.to raise_error
  end

  it 'consumes valid event' do
    expect { subject.consume(valid_event) }.to_not raise_error
  end
end

# user_creation_consumer_spec.rb
describe UserCreationConsumer do
  let(:invalid_event) { SpamEvent.new  }
  let(:valid_event) { UserEvent.new  }

  it_should_behave_like 'event consumer'
end

# spam_report_consumer_spec.rb
describe SpamReportedConsumer do
  let(:invalid_event) { UserEvent.new  }
  let(:valid_event) { SpamEvent.new  }

  it_should_behave_like 'event consumer'
end

The shared examples in the first snippet depend on two symbols, invalid_event and valid_event. How these symbols are defined depends on when it_should_behave_like 'event consumer' is called.

To make matters worse, it is very rare that a test case is as simple as the minimal example above. Often you see things like this:

# user_creation_consumer_spec.rb
describe UserCreationConsumer do
  let(:transaction_type) { stub('transaction type')  }
  let(:invalid_event)    { SpamEvent.new  }
  let(:valid_event)      { UserEvent.new  }
  let(:observer)         { FakeObserver.new  }
  let(:event)            { stub('some event')  }

  it_should_behave_like 'event consumer'
  it_should_behave_like 'observable'
  it_should_behave_like 'component'

  it 'logs the user creation somewhere' do
    #...
  end
end

Now, if I want to do some simple refactoring, like renaming the :event binding, I have to go through all the example groups and see what uses that.

Eventually, my tests will become so hard to maintain I will pretty much wrap each it_should_behave_like block into its own describe or context block, and maybe define some naming conventions around variables consumed by a given shared example. This is exactly the same kind of overhead we need to apply when using dynamic scope, as described by the article linked above.

Making it Explicit

When I first had to work on a big code base with tests relying on dynamic scope, I spent a couple of hours thinking about how to bypass what I saw as a limitation of RSpec. Then I realised something: even though you cannot make what you define in your spec not visible by the shared group, you can be explicit about what you pass to the argument groups.

You can pass an initialisation block, which will be executed before the specs on that shared group. This doesn't require any change to the shared group itself, we just need to change our specs a bit:

describe UserCreationConsumer do
  it_should_behave_like 'event consumer' do
    let(:invalid_event) { SpamEvent.new  }
    let(:valid_event) { UserEvent.new  }
  end
end

describe SpamReportedConsumer do
  it_should_behave_like 'event consumer' do
    let(:invalid_event) { UserEvent.new  }
    let(:valid_event) { SpamEvent.new  }
  end
end

Or you can declare your shared group in a way that receives parameters, just like any other function call would:

shared_examples_for 'event consumer' do |valid_event, invalid_event|
  it 'raises error if asked to consume invalid event' do
    expect { subject.consume(invalid_event) }.to raise_error
  end

  it 'consumes valid event' do
    expect { subject.consume(valid_event) }.to_not raise_error
  end
end

describe UserCreationConsumer do
  it_should_behave_like 'event consumer', UserEvent.new, SpamEvent.new
end

describe SpamReportedConsumer do
  it_should_behave_like 'event consumer', SpamEvent.new, UserEvent.new
end

The resulting code not only is smaller, but easier to follow and refactor.

I am not sure why people don't use this more often, maybe because people are so used to everything being global in Rails that they don't think too much of it.