Shoulda used RSpec

I’ve been updating the muck gems to work with Rails 3. The change is significant and painful but in a good way. For the most part the change consists of deleting all the code you used to hack into the Rails framework and adding subclass of ‘::Rails::Engine’. I’ll try to put together a detailed post explaining Rails 3 engines later.

The nice thing about having a test suite in place is that you can rely on those tests to ensure the upgrade is smooth. I’ve always written my tests in Shoulda. I like the syntax and I love the macros. However, I’m switching everything over to RSpec. Thoughtbot has declared they won’t abandon the Shoulda community, but since they’ve made the switch to RSpec for new projects it’s pretty clear that Shoulda as a stand alone project has a limited life.

Luckily I’ve used RSpec in a lot of other projects and it’s an easy DSL to like. The conversion from shoulda to RSpec isn’t a terribly difficult one. You basically need to replace “context” with describe and then remove your assert statements and replace then with ‘should’.

One of the most difficult issues I’ve had to deal with is my custom shoulda macros. From what I’ve read the move to BDD should inspire you to use fewer macros and instead focus on specific behavior. I think that philosophy is great, but I’ve already got a lot of code that uses the macros and so the macros need to be brought over. The change from shoulda macros to RSpec matchers is painful, but I think the code is cleaner and easier to read.

For example, I have custom scope macros. Here’s the shoulda macro:


  # Test for 'by_title' named scope which orders by title:
  # scope :by_title, :order => "title ASC"
  # requires that the class have a shoulda factory
  def should_scope_by_title
    klass = get_klass
    factory_name = name_for_factory(klass)
    context "'by_title' title scope" do
      setup do
        klass.delete_all
        @first = Factory(factory_name, :title => 'a')
        @second = Factory(factory_name, :title => 'b')
      end
      should "sort by name" do
        assert_equal @first, klass.by_title[0]
        assert_equal @second, klass.by_title[1]
      end
    end
  end
</pre>

And here's the RSpec matcher:


# Ensures that the model can sort by_title
# requires that the class have a factory
#
# Tests scope:
#   scope :by_title, :order => "title ASC"
#
# Examples:
#   it { should scope_by_title }
#
def scope_by_title
  SortingMatcher.new(:by_title, :title)
end

class SortingMatcher # :nodoc:

  def initialize(scope, field)
    @scope = scope
    @field = field
  end

  def matches?(subject)
    @subject = subject
    @subject.class.delete_all
    @first = Factory(factory_name, @field => 'a')
    @second = Factory(factory_name, @field => 'b')
    @first == @subject.class.send(@scope)[0] && @second == @subject.class.send(@scope)[1]
  end

  def failure_message
    "Expected #{factory_name} to scope by #{@scope} on #{@field}"
  end

  def description
    "sort by title"
  end

  private

    def factory_name
      @subject.class.name.underscore.to_sym
    end

end
</pre>

I don't profess to be a master of either but my tests work so hopefully this helps someone if you're making the same transition.

Leave a Reply