Custom Shoulda macros — a tutorial

Shoulda gem is great not only because it provides you with a very clean and natural way of organizing tests with context and should building blocks, but it also comes with quite a large set of predefined macros that mirror Rails’ validations, relations etc. What’s even better — it’s very easy to create your own macros to further speed up test writing. Modern versions of Shoulda gem allow to do it in a clean and modular way. That’s great news if you are serious about TDD because for every substantial codebase you will end up with even bigger pile of testing code, so any tool helping in encapsulating common test logic or patterns is priceless.

This article is a tutorial on writing custom Shoulda macros: from very simple to quite complex.

A note on structure

First things first: your custom macros should be placed in a Ruby file in test/shoulda_macros/ directory (assuming it’s a Rails project). You can have as many files as you like so I recommend grouping macros by their theme or usage. Having small, coherent modules of related macros makes for easier reuse in other projects.

Inside the file you basically add class methods to Test::Unit::TestCase so the basic pattern is:

class Test::Unit::TestCase
  def self.should_be_awesome
    should "be awesome" do
      # actual test code here
    end
  end

  # even more Shoulda awesomeness...
end

The methods you create here can either call various assert_... methods or use Shoulda’s building blocks should and context. The latter approach is recommended (read: totally cool) because then your macros will fit nicely into Shoulda framework, providing nice test names and informative failure messages.

Simple macro

So, how about a simple macro for starters? For example, let’s write a macro to assert that the tested class has an attr_reader:

class Test::Unit::TestCase
  def self.should_have_attr_reader name
    klass = self.name.gsub(/Test$/, '').constantize
    should "have attr_reader :#{name}" do
      obj = klass.new
      obj.instance_variable_set("@#{name}", "SomeSecretValue")
      assert_equal("SomeSecretValue", obj.send(name))
    end
  end
end

# Usage
class PostTest
  should_have_attr_reader :comments_number
end

First (line 3) we need to get the class under test. Next line is where things get interesting — a should block is created. Please note how we include the name parameter in the block’s description. This way we get more information in case of failure and can actually use the macro more than once per class (otherwise Shoulda would complain about duplicate test names). Inside the should block there’s the actual testing code, which is irrelevant to this tutorial, so I’ll spare you the details. Just remember to actually assert something inside :)

Reusing built-in & custom macros

As an additional bonus, we can reuse our macros in other macros, so had we create a should_have_attr_writer in a similar manner, we could also get:

  def self.should_have_attr_accessor name
    should_have_attr_reader name
    should_have_attr_writer name
  end

We can also reuse built-in macros, for example to create should_validate_inclusion_of, which for some reason is not available in Shoulda. Of course, in general it’s not possible to test all the values that should not be allowed, but I decided to bite the bullet and came up with this:

def self.should_validate_inclusion_of a, opts
  lst = opts[:in]
  should_allow_values_for a, *lst
  rng = (lst.min.pred..lst.max.succ)
  invalid = (rng.to_a + [nil, "", 0, 1, 2]) - lst
  should_not_allow_values_for a, *invalid
end

# Usage
class NumberTest
  should_validate_inclusion_of :powers_of_two, :in => [1, 2, 4, 8, 16]
end

In lines 2 & 3, we get our list of allowed values and hand it to Shoulda’s own should_allow_values_for. So far so good, but how to get invalid values? Again, this is not relevant to this tutorial, but in short I assume that the values are numbers and create a little bit wider list then remove the valid values. That gives us a list of values that are not allowed for the attribute and all we need to do is call should_not_allow_values_for on them.

should_have_attr_reader revisited

Of course we are not limited to one should block per macro, so we can improve our should_have_attr_reader to accept any number of params:

class Test::Unit::TestCase
  def self.should_have_attr_reader *names
    klass = self.name.gsub(/Test$/, '').constantize
    names.each do |name|
      should "have attr_reader :#{name}" do
        obj = klass.new
        obj.instance_variable_set("@#{name}", "SomeSecretValue")
        assert_equal("SomeSecretValue", obj.send(name))
      end
    end
  end
end

# Usage
class PostTest
  should_have_attr_reader :yes, :i_really, :have, :that, :many, :attr_readers
end

The only thing new here is each loop on line 4 that creates as many should blocks as there are parameters given to our macro. This is better that one should block with a loop iterating over parameters inside it, because all attr_readers are tested independently and one failure doesn’t prevent others from running.

Advanced

And now for even more contrived advanced example. This time it’s should_delegate which tests if some methods are delegated (Rails style) to some field:

def self.should_delegate *args
  if args.last.is_a? Hash
    opts = args.pop
    field = opts[:to]
    args.each { |name| should_delegate name, field }
    return
  else
    name, field = args
  end

  klass = self.name.gsub(/Test$/, '').constantize
  to_class = field.to_s.camelize.constantize

  should "delegate #{name} to #{to_class}" do
    to = to_class.new
    to.expects(name).returns "name_value"
    obj = klass.new field => to
    assert_equal("name_value", obj.send(name))
  end
end

# Usage

class Comment
  belongs_to :post
  delegate :category, :to => :post
end

class CommentTest
  # simple case
  should_delegate :category, :post
  # "advanced" case
  should_delegate :category, :more, :attrs, :to => :post
end

In the simple case one should block is created to test if the method in question is delegated to the field. In the “advanced” case, there are more attributes and the field is specified as the last argument preceded by a :to => to make it more DSL-ish. With these arguments, should_delegate pops the hash off the argument list, extracts the :to option and then calls itself recursively on all remaining arguments, thus reusing the “simple case”.

One-off cases

Defining custom macros doesn’t have to be done in separate files. If your test logic is specific to only one method or class, you can define them right there in the test class. I do it whenever I notice some pattern emerge in my tests that is too specific for a full-blown custom macro. For example, here’s a context block for some helper method:

context "class_for_value" do
  should "equal 'bullet_cross_icon' for false" do
    assert_equal('bullet_cross_icon', class_for_value(false))
  end

  should "equal 'bullet_cross_icon' for 0" do
    assert_equal('bullet_cross_icon', class_for_value(0))
  end

  should "equal 'tick_icon' for true" do
    assert_equal('tick_icon', class_for_value(true))
  end

  should "equal 'tick_icon' for 1" do
    assert_equal('tick_icon', class_for_value(1))
  end

  should "equal nil for 2" do
    assert_equal(nil, class_for_value(2))
  end

  should "equal nil for 10" do
    assert_equal(nil, class_for_value(10))
  end
end # class_for_value

Pretty boring and tedious to write and maintain. Why not refactor this with Customo Macroculus?

context "class_for_value" do
  def self.should_equal expected, value
    should "equal '#{expected}' for #{value}" do
      assert_equal(expected, class_for_value(value))
    end
  end

  should_equal 'bullet_cross_icon', false
  should_equal 'bullet_cross_icon', 0
  should_equal 'tick_icon',         true
  should_equal 'tick_icon',         1
  should_equal nil,                 2
  should_equal nil,                 10
end

Ah, much better now. And that concludes this mini tutorial. Be sure to check out http://wiki.github.com/thoughtbot/shoulda/example-macros and http://mileszs.com/blog/2008/09/21/shoulda-macros-for-common-plugins.html for more examples, including creating contexts inside your macros. There are many collections of useful Shoulda macros on Github and the Internet, try googling — maybe there’s even one to do what you need? You wouldn’t have to read this boring tutorial then :)

About these ads

4 responses to “Custom Shoulda macros — a tutorial

  • Jarl Friis

    Fantastic tutorial. Just what I was looking for. And then you are also a great author. This kind of stuff should be part of the shoulda documentation.

  • Michael Barton

    Agree with the above comment. This was really helpful and should be part of the documentation.

  • Snuggs

    How can you test that an object has arguments or not. This code worked PERFECTLY until I came across one of my objects that needed an argument passed to new :-/

  • szeryf

    @Snuggs: you can use some special key in your options hash, for example:

      should_delegate :category, :to => :post, :args_for_new => [1, 2, 3]
    

    Then use it in the should_delegate method:

      def self.should_delegate *args
         # snip...
         should "delegate #{name} to #{to_class}" do
           args_for_new = opts[:args_for_new] || []
           to = to_class.send(:new, args_for_new)
     

    I hope you get the idea :)

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: