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.