Custom ArgumentMatchers in rSpec

written by trotter on February 12th, 2009 @ 06:55 PM

Ok, so you know the stuff you pass into #with when doing stubs and mocks in rSpec? Those are called ArgumentMatchers. Most of the time you’re writing code like obj.should_receive(:blah).with('a string'), but you can get fancier and use rSpec’s built in ArgumentMatchers to do other nice things like obj.should_receive(:blah).with(hash_including(:key => 'val')), which will match a call like obj.blah(:foo => 'bar', :key => 'val').

This is neat and all, but rSpec’s ArgumentMatchers don’t always go as far as you’d like. In particular, there’s a hash_including, but no array_including. In the rest of this post, I’m going to show you how to write your own expectations, using array_including as an example. It turns out to be surprisingly simple.

So first, a word of warning, in rSpec 1.1.12 ArgumentMatchers are called ArgumentConstraints. Replace all occurrences of ArgumentMatchers below with ArgumentConstraints, unless you’re on rSpec edge.

With that disclaimer out of the way, open your spec_helper.rb, where we’re going to add an ArrayIncludingMatcher to the Spec::Mocks::ArgumentMatchers namespace. We will define an initialize method to take and store the expected value, and == method to compare against the actual value, and a description method to print out a handy description when the test fails.

module Spec
  module Mocks
    module ArgumentMatchers
      class ArrayIncludingMatcher

        # We'll allow an array of arguments to be passed in, so that you can do
        # things like obj.should_receive(:blah).with(array_including('a', 'b'))
        def initialize(*expected)
          @expected = expected
        end

        # actual is the array (hopefully) passed to the method by the user.
        # We'll check that it includes all the expected values, and return false
        # if it doesn't or if we blow up because #include? is not defined.
        def ==(actual)
          @expected.each do |expected|
            return false unless actual.include?(expected)
          end
          true
        rescue NoMethodError => ex
          return false
        end

        def description
          "array_including(#{@expected.join(', ')})" 
        end

      end

      # array_including is a helpful wrapper that allows us to actually type
      # #with(array_including(...)) instead of ArrayIncludingMatcher.new(...)
      def array_including(*args)
        ArrayIncludingMatcher.new(*args)
      end

    end
  end
end

Note that we also defined an array_including method as the readable wrapper. For symettry’s sake, we should also define ArrayNotIncludingMatcher, which I’ve included in the following gist. Feel free to copy this matcher, but I’d love to see you guys creating your own. Leave links to gists in the comments if you come up with anything fun!

Update: I had to remove the embedded gist because it was hanging the page when github was down. Check out the link if you can: http://gist.github.com/62943.js.

Comments are closed

Options:

Size

Colors