Custom Matchers for Matchy

I’ve been using Shoulda for around a month now and, thus far, the only thing I have missed is RSpec’s matchers (ie: foo.should == 2). Enter stage right Jeremy McAnally’s project matchy. Matchy provides “RSpec-esque matchers for Test::Unit”.

Matchy met the immediate need of liking the should and should_not RSpec syntax, but, originally, it was not built with support for custom matchers. A little over a month ago, Matthias Hennemeyer abstracted the ability to build matchers and created a method called def_matcher to create your own custom matchers.

Excited, I installed the new version and went to town, only to end up confused. def_matcher only seemed to work from inside an instance method. Most likely I was just too stupid to use what he created, but I figured if I was too stupid there were probably others. Instead of giving up, I hacked a version into my project of what Matthias created that I called custom_matcher and made it work more like a class method.

Normally, hacking what I need into my project is where it stops, but this time I decided to give back and actually fork matchy and apply my changes. Today while waiting on some stuff from a client, I took the time to actually add my changes, test them and push them upstream to Github.

So enough boring explanation, eh? How do the changes work I’m sure you are wondering. For each example, I’ll show an example with and without the custom_matcher stuff so you can see the API difference.

Testing Nil

The first custom matcher that I added was straight up ganked from RSpec and is named be_nil. Nothing fancy, but I kind of like it.

class ActiveSupport::TestCase
  custom_matcher :be_nil do |receiver, matcher, args|
    matcher.positive_failure_message = "Expected #{receiver} to be nil but it wasn't"
    matcher.negative_failure_message = "Expected #{receiver} not to be nil but it was"
    receiver.nil?
  end
end

class ItemTest < ActiveSupport::TestCase    
  def test_something
    item = Item.new
    # without custom matcher
    item.title.should be(nil)

    # with custom matcher
    item.title.should be_nil
  end
end

So the difference is slight, right? I’m all kinds of lazy so the difference of typing be_nil versus be(nil) is worth it. One underscore is easier than two parenthesis. Heck, underscore is even easier to spell as a word, but that is probably irrelevant.

Custom Matcher Syntax

Before I go on to more examples, let’s make sure you understand what was happening above. The basic syntax of a custom_matcher is:

custom_matcher :matcher_name do |receiver, matcher, args|
  # matcher body
end

:matcher_name is pretty obvious but what is the purpose of the receiver, matcher and args in the block? Receiver is the object that the modal (should, should_not) is being called on. In the example above item.title would be the available as the receiver in the custom_matcher block.

Matcher has a couple purposes. First, it allows you to change the failure messages, both positive (should) and negative (should not). If you say item.title.should be_nil and item.title is not nil, the failure message will be equal to the positive_failure_message. Likewise, if you say item.title.should_not be_nil and item.title is nil, the negative_failure_message is what you’ll see. Matcher also allows some cool chaining of messages onto the matcher and I’ll show that in a bit.

Finally, args is equal to the arguments (if any) that are passed into the matcher method. This allows for some really cool test API tweaks.

Let’s use that new knowledge of matcher and args to create a matcher that takes advantage of both, and hopefully shows the power of custom matchers. In this example, we’ll create a custom matcher :have that allows testing the size of an array returned by a method on the receiver.

class Test::Unit::TestCase
  custom_matcher :have do |receiver, matcher, args|
    count = args[0]
    something = matcher.chained_messages[0].name
    actual = receiver.send(something).size
    actual == count
  end
end

class MoreAdvancedTest < Test::Unit::TestCase
  class Item
    def tags
      %w(foo bar baz)
    end
  end

  def test_item_has_tags
    item = Item.new
    # without custom matcher
    item.tags.size.should == 3

    # with custom matcher
    item.should have(3).tags # pass
    item.should have(2).tags # fail
  end
end

In the custom_matcher above, matcher.chained_messages0.name is equal to “tags”. So basically the :have custom matcher gets the chained method (tags), calls it on the receiver (item) and then makes sure that the result size (item.tags.size) is equal to the argument passed into :have (3 and 2).

Maybe a more simple example of passing in arguments could be the following have_error_on matcher:

class ActiveSupport::TestCase
  custom_matcher :have_error_on do |receiver, matcher, args|
    attribute = args[0]

    receiver.valid?
    receiver.errors.on(attribute).should_not be(nil)
  end
end

class Item < ActiveRecord::Base
  validate_presence_of :title
end

class ItemTest < ActiveSupport::TestCase 
  def test_title_is_required
    item = Item.new

    # without custom matcher
    item.valid?
    item.errors.on(:title).should_not be(nil) # or something like this

    # with custom matcher
    item.should have_error_on(:title)
  end
end

Hopefully these examples are good enough to get an idea of what is going on. The basic idea is that you can create custom matcher methods, which can call methods on the receiver, customize the error messages, and even accept arguments. Again, the internals of the matcher building were written by Matthias. All I did was put it together in a way that made sense and seemed easier to me (and hopefully others).

Jeremy may or may not pull my changes into his official matchy repository, so if you want to use them now, revel in the power of Github by installing my version gem (sudo gem install jnunemaker-matchy). Then be sure to require the gem in your test_helper. Here is an example Rails test_helper with a few matchers already in it.

4 Comments

  1. Yeah I’m totally pulling these changes in. Been on a vaguely unplugged vacation for a little over a week, so been trying to avoid doing much with code (though I did release sweep, so I guess I couldn’t quit cold turkey :)).

  2. @Jeremy – Coolio. I just got back from a vaguely unplugged vacation too. :)

  3. Out of curiousity, have you used shoulda with matchy? Having ‘should’ for describing tests (from shoulda), and ‘should’ for using matchers would make my head hurt. No problem using it with Jeremy’s context project though, since you can use ‘it’ and ‘specify’ for describing tests.

    I also did a little poking around with matchy to make it be API compatible with RSpec matchers. Feasibly, you could use any existing RSpec matcher with it. The only problem I had was that I couldn’t get it to work with mocha installed (due to how matchy and mocha both reach down into test/unit’s internals). Fork is here: http://github.com/technicalpickles/matchy/tree/master

  4. @Josh – Yeah I typically use shoulda and matchy together. Doesn’t make my brain hurt at all. I like should ‘…’ do end for test declaration and I really like should/should_not for matchers like RSpec.

Sorry, comments are closed for this article to ease the burden of pruning spam.

About

Authored by John Nunemaker (Noo-neh-maker), a web developer and programmer who has fallen deeply in love with Ruby. More about John.

Syndication

Feed IconRailsTips Articles - An assortment of howto's and thoughts on Ruby and Rails.

Feed IconRails Quick Tips - Ruby and Rails related links.