Is there a concept of an implicit subject to be passed to an rspec matcher?
Say I have a custom matcher that checks to see if an object is a valid RGB tuple -- let's call it valid_rgb_tuple
. The matcher takes an object as an argument and validates it. I'd like to be able to use that in a oneliner where the subject
is expected to be a tuple. Ideally that would look/read something like this:
subject { some_method_that_should_return_a_tuple }
it { should be_a_valid_rgb_tuple }
I'm not sure how to make that work, or if it's even possible to get close. I think I need a way to force the subject to get automatically passed into the validator. Is there a construct for this?
UPDATE:
This turned out just to be a problem with the way I defined the matcher. I did something like this:
RSpec::Matchers.define :be_a_valid_rgb_tuple do |color|
match do
begin
# check the color value
end
end
end
Which is incorrect because of how color
is passed. It needs to be redone like so:
RSpec::Matchers.define :be_a_valid_rgb_tuple do
match do |color|
begin
# check the color value
end
end
end
Note how the value is passed to the block provided to the match method. In the first case, the color argument is an argument to the call to be_a_valid_rgb_tuple
that creates the matcher, whereas in the second it's the implicit subject itself.
should be_a_valid_rgb_tuple
is just expect(subject).to be_a_valid_rgb_tuple
. It might seem a little less magical with parenthesis.
expect(subject()).to(be_a_valid_rgb_tuple())
expect(subject())
just saves the return value of subject
in an object and returns it. to
is called on that with the matcher object returned by be_a_valid_rgb_tuple
and it passes the stored return value to the matcher.
Here is a very rough sketch of how rspec works.
class Expectation
def initialize(value)
@value = value
end
def to(matcher)
raise unless matcher.matches?(@value)
end
end
class ValidRgbTupleMatcher
def matches?(value)
return false unless value.is_a?(RgbTuple)
return false unless value.valid?
return true
end
end
class RgbTuple
def valid?
true
end
end
def expect(value)
Expectation.new(value)
end
def subject
RgbTuple.new
end
def be_a_valid_rgb_tuple
ValidRgbTupleMatcher.new
end
expect(subject).to be_a_valid_rgb_tuple
You'd write be_a_valid_rgb_tuple
as a custom matcher and use the normal one liner syntax. Try to write up the matcher using the rspec docs and if you have trouble add a specific question.
Though you probably don't need a custom matcher. Instead, use be_a
to check the class of the return value, and be_valid
to check if its valid. Combine them with and
.
it { should be_a(RgbTuple).and be_valid }
Equivalent to...
it {
tuple = subject
expect(tuple.is_a?(RgbTuple)).to be true
expect(tuple.valid?).to be true
}
Matchers don't actually care about the implicit subject. Matchers just take a actual value and optionally an expected value.
require 'rspec/expectations'
RSpec::Matchers.define :be_a_multiple_of do |expected|
match do |actual|
actual % expected == 0
end
end
The implicit subject really is just a matter of how the matcher is called. If you do:
subject { 16 }
it { should be_a_multiple_of 4 }
Its just a shorthand for expect(subject).to(be_a_multiple_of(4))
.
A minimal implementation of a matcher for a "RGB tuple" is:
require 'rspec/expectations'
RSpec::Matchers.define :be_a_valid_rgb_tuple do
match do |actual|
# @todo the boring work of handling bad input such as nil
# and providing better feedback
actual.select { |n| n.is_a?(Integer) && (0..255).cover?(n) }.length == 3
end
end
This assumes that by valid "RGB tuple" you mean an array with three integers between 0 and 255.