Array matcher 3
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(//) && mail.body.match(//)
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(//) && mail.body.match(//))
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:
= "test"
# my_array.should_match_at_least_one { |item| item.match(/abc/) && item.match(/def/) }
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(//) && mail.body.match(//)
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.
Trackbacks
Use the following link to trackback from your own site:
http://inter-sections.net/trackbacks?article_id=array-matcher&day=26&month=11&year=2007




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.shouldnot beempty
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.shouldnot benil
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 atleastone_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 atleastone_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.