Custom Matchers
We’ve talked a bit about matchers before, and briefly mentioned that RSpec even allows us to define our own custom matchers.
Let’s have a quick look at this.
Here is the code that we have so far:
require "date"
def leap_year?(year)
year % 400 == 0 or year % 100 != 0 and year % 4 == 0
end
class User
def initialize(name, birthday)
@name = name
@birthday = birthday
end
def name
@name
end
def born_in_leap_year?
leap_year?(Date.parse(@birthday).year)
end
end
describe User do
subject { User.new("Francisca", "#{year}-01-01") }
context "born in 2001" do
let(:year) { 2001 }
it { should_not be_born_in_leap_year }
end
context "born in 1900" do
let(:year) { 1900 }
it { should_not be_born_in_leap_year }
end
context "born in 2000" do
let(:year) { 2000 }
it { should be_born_in_leap_year }
end
context "born in 2004" do
let(:year) { 2004 }
it { should be_born_in_leap_year }
end
end
Now, what if we want to specify (test) the name
method?
We could simply add the following test, using the basic style:
describe User do
subject { User.new("Francisca", "#{year}-01-01") }
context "born in 2001" do
it "returns the name" do
expect(subject.name).to eq "Francisca"
end
it { should_not be_born_in_leap_year }
end
end
However, we’d mix styles here. That’s not a bad thing, really! But wouldn’t it be cool if we could say this instead?
describe User do
subject { User.new("Francisca", "#{year}-01-01") }
context "born in 2001" do
let(:year) { 2001 }
it { should be_named("Francisca") }
it { should_not be_born_in_leap_year }
end
end
If we were to execute this, RSpec would try to call the method named?
on our
User
instance (just like be_born_in_leap_year
calls born_in_leap_year?
on
the user), and that method does not exist. We could add that method named?
to
our User
class, but we don’t really want to add any such methods to our real
code, just so we can make the tests prettier.
Instead, we can define a custom matcher
be_named
that inspects the user’s name
:
RSpec::Matchers.define(:be_named) do |expected|
match do |object|
object.name == expected
end
end
Hmmmm, … apparently a matcher is a block that calls a method match
that
takes another block. The actual and expected values are passed as arguments to
the two blocks, somehow. Inside the inner block we are supposed to return
true
or false
depending if the matcher is supposed to “match”.
Ok, well, we don’t really have to understand how exactly this works in detail—we can just slap it at the end of our file, and run it:
$ rspec --format doc user_spec.rb
User
born in 2001
should be named "Francisca"
should not be born in leap year
Finished in 0.00267 seconds (files took 0.15704 seconds to load)
2 examples, 0 failures
Yay!
Now, how cool is that.
With those five lines of Ruby code we’ve extended RSpec to include a matcher
that is pretty specific to our code. And now we can use the advanced style
in order to test our name
method.