Monday, November 26th, 2007...8:04 pm

Array matcher


Jump to Comments

Here’s a lovely little snippet that shows the reasons why Ruby is such a nice language to work with. It’s to do with matching at least one of a series of items in RSpec.

Step 1: First, it was simple

Then(user $email should receive an email with his confirmation link) do |email|
  mail = ActionMailer::Base.deliveries.last
  user = User.find_by_email(email)
  mail.body.match(/#{user.confirmation_code}/) && mail.body.match(/#{user.id}/)
end

This is my simple matcher that I started with… The world was simple, and this worked fine. But then, something happened. The world started sending more than a single email in one Story, and so the need for matching multiple emails was born.

Step 2: Then, it was easy, but messy

Then(user $email should receive an email with his confirmation link) do |email|
  matches = 0
  ActionMailer::Base.deliveries.each do |mail|
    user = User.find_by_email(email)
    matches += 1 if (mail.body.match(/#{user.confirmation_code}/) && mail.body.match(/#{user.id}/))
  end
  matches.should == 1
end

This works. It works well. It works fine. But, it’s ugly. It’s not readable. It’s not RSpec-like, and it stands out like a sore thumb in the middle of my step matchers. I have Ruby to thank for the fact that fugly hacks like this stand out and beg to be rewritten in a neater way. Other languages, such as PHP, do not have this quality, and so hacks remain untouched for ages and ages.

I could not let things rest in this state. Ugliness like that must be removed. Trying to match any one of an array of items is one of those things that are likely to be repeated a few times, so I decided to extract that out. This is where another one of Ruby’s qualities came forth, and made it child’s play to extract this concept and stick it where it belongs - in Enumerable (but, I hasten to add, only when running specs…).

Step 3: Then, it was simple again

Enter my latest addition to /stories/helper.rb:

ENV[RAILS_ENV] = test
require File.expand_path(File.dirname(__FILE__) + /../config/environment)
require spec/rails/story_adapter

module Enumerable
  # my_array.should_match_at_least_one { |item| item.match(/abc/) && item.match(/def/) }
  def at_least_one_should
    matches = 0
    self.each do |item|
      matches +=1 if yield item
    end
    matches.should >= 1
  end
end

Which means that the step matcher can now been reduced to the reasonably elegant and expressive:

Then(user $email should receive an email with his confirmation link) do |email|
  user = User.find_by_email(email)
  ActionMailer::Base.deliveries.at_least_one_should do |mail|
    mail.body.match(/#{user.confirmation_code}/) && mail.body.match(/#{user.id}/)
  end
end

You might be tempted to point out that I could probably use String#include? instead of String#match, since I do not care about the actual matching, only about the presence… and you’d be right. But, like an imperfectly perfect persian rug, I’m happy with my code like this.

[?]
Share

3 Comments

  • Not 100% sure I like this more or not, but what about this?

    ActionMailer::Base.deliveries.select do |mail|
    mail.body.match(/#{user.confirmation_code}/) &&
    mail.body.match(/#{user.id}/)
    end.should_not be_empty

    or, so we don’t do unnecessary checks…

    ActionMailer::Base.deliveries.detect do |mail|
    mail.body.match(/#{user.confirmation_code}/) &&
    mail.body.match(/#{user.id}/)
    end.should_not be_nil

  • Interesting :-) Another twist on the approach.

    I like yours because it doesn’t require any meta-programming, but I still think my version is more readable. Only just though… I guess it’s down to taste. ;-)

    Thanks for the comment.

  • I prefer to see blocks collect data instead of modifying external variables:

    module Enumerable
    def sum ; inject(0) { |x, y| x + y }; end

    def at_least_one_should
    matches = self.collect do |item|
    1 if yield item
    end.compact.sum
    matches.should >= 1
    end
    end

    But that’s overkill to test “at least one”, which I’d write:
    module Enumerable
    def at_least_one_should
    self.each do |item|
    return true if yield item
    end
    return false
    end
    end

    I’m not familiar with your test framework so amybe that ’should’ test is raising something or setting a variable instead of returning true/false, but I’d rather express that I’m trying to find at least one by returning as soon as I find one. And it saves the extra checks.

Leave a Reply

Close
E-mail It