March 24, 2009
Older: HTTParty Example: MyMileMarker.com
Newer: Building API Wrapping Gems Could Not Get Much Easier
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
Mar 25, 2009
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 :)).
Mar 25, 2009
@Jeremy – Coolio. I just got back from a vaguely unplugged vacation too. :)
Mar 27, 2009
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
Mar 27, 2009
@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.