Delegate Matcher
An RSpec matcher for validating delegation. This matcher works with delegation based on the Forwardable module, the delegate method in the Active Support gem or with simple custom delegation.
Installation
Add this line to your application's Gemfile:
gem 'delegate_matcher'And then execute:
$ bundleOr install it yourself as:
$ gem install delegate_matcherThen add the following to your spec_helper.rb file:
require 'delegate_matcher'Usage
This matcher allows you to validate delegation to:
- instance methods
- class methods
- instance variables
- class variables
- constants
- arbitrary objects
- multiple targets
describe Post do
it { should delegate(:name).to(:author) } # name => author().name instance method
it { should delegate(:name).to(:class) } # name => self.class.name class method
it { should delegate(:name).to(:@author) } # name => @author.name instance variable
it { should delegate(:name).to(:@@author) } # name => @@author.name class variable
it { should delegate(:first).to(:GENRES) } # first => GENRES.first constant
it { should delegate(:name).to(author) } # name => author.name object
endDelegate Method Name
If the name of the method being invoked on the delegate is different from the method being called you
can check this using the with_prefix method (based on Active Support delegate method) or the
as method.
describe Post do
it { should delegate(:name).to(:author).with_prefix } # author_name => author.name
it { should delegate(:name).to(:author).with_prefix(:writer) } # writer_name => author.name
it { should delegate(:writer).to(:author).as(:name) } # writer => author.name
endNote: if you are delegating to an object (i.e. to is not a string or symbol) then a prefix of '' will be used:
describe Post do
it { should delegate(:name).to(author).with_prefix } # an error will be raised
it { should delegate(:name).to(author).with_prefix(:writer) } # writer_name => author.name
endYou can test delegation to a chain of objects by using a to that can be eval'd.
describe Post do
it { should delegate(:name_length).to(:'author.name').as(:length) } # name_length => author.name.length
it { should delegate(:length).to(:'author.name').with_prefix(:name) } # name_length => author.name.length
endHandling Nil Delegates
If you expect the delegate to return nil when the delegate is nil rather than raising an error
then you can check this using the allow_nil method.
describe Post do
it { should delegate(:name).to(:author).allow_nil } # name => author && author.name
it { should delegate(:name).to(:author).allow_nil(true) } # name => author && author.name
it { should delegate(:name).to(:author).allow_nil(false) } # name => author.name
endNil handling is only checked if allow_nil is specified.
Note that the matcher will raise an error if you use this when checking delegation to an
object since the matcher cannot validate nil handling as it cannot gain access to the subject
reference to the object to set it nil.
Arguments
If the method being delegated takes arguments you can supply them with the with method. The matcher
will check that the provided arguments are in turn passed to the delegate.
describe Post do
it { should delegate(:name).with('Ms.').to(:author) } # name('Ms.') => author.name('Ms.')
endAlso, in some cases, the delegator change the arguments. While this is arguably no
longer true delegation you can still check that arguments are correctly passed by using a second with
method to specify the arguments expected by the delegate.
describe Post do
it { should delegate(:name).with('Ms.').to(:author).with('Miss') } # name('Ms.') => author.name('Miss')
endYou can use the with(no_args). rather than with. or with(). to test delegation with no arguments passed
to the delegator.
describe Post do
it { should delegate(:name).with(no_args).to(:author) } # name() => author.name()
endFor the second with you can use any RSpec argument matcher.
describe Post do
it { should delegate(:name).with('Ms.').to(:author).with(anything) } # name('Ms.') => author.name(*)
endBlocks
You can check that a block passed is in turn passed to the delegate via the with_block method.
By default, block delegation is only checked if with_a_block or without_a_block is specified.
describe Post do
it { should delegate(:name).to(author).with_a_block } # name(&block) => author.name(&block)
it { should delegate(:name).to(author).with_block } # name(&block) => author.name(&block) # alias for with_a_block
it { should delegate(:name).to(author).without_a_block } # name(&block) => author.name
it { should delegate(:name).to(author).without_block } # name(&block) => author.name # alias for without_a_block
endYou can also pass an explicit block in which case the block passed will be compared against the block received by the delegate. The comparison is based on having equivalent source for the blocks.
describe Post do
it { should delegate(:tainted?).to(authors).as(:all?).with_block { |a| a.tainted? } } # tainted? => authors.all? { |a| a.tainted? }
endIf the proc passed to with_block is not the same proc used y the delegator
then the sources of the proc's will be compared for equality using the proc_extensions gem.
For this comparison to work the proc must not be not have been created via an eval of a string
and must be the only proc declared on that line of source code.
See proc_extensions for more details on how the source for procs is compared.
Return Value
Normally the matcher will check that the value return is the same as the value
returned from the delegate. You can skip this check by using without_return.
describe Post do
it { should delegate(:name).to(:author).without_return }
endMultiple Targets
You can test delegation to more than one target by specifing more than one target via to.
require 'spec_helper'
module RSpec
module Matchers
module DelegateMatcher
module Composite
describe 'delegation to multiple targets' do
class Post
include PostMethods
attr_accessor :authors, :catherine_asaro, :isaac_asimov
def initialize
self.catherine_asaro = Author.new('Catherine Asaro')
self.isaac_asimov = Author.new('Isaac Asimov')
self.authors = [catherine_asaro, isaac_asimov]
end
def name
authors.map(&:name)
end
end
subject { Post.new }
it { should delegate(:name).to(*subject.authors) }
it { should delegate(:name).to(subject.catherine_asaro, subject.isaac_asimov) }
it { should delegate(:name).to(:catherine_asaro, :isaac_asimov) }
it { should delegate(:name).to(:@catherine_asaro, :@isaac_asimov) }
end
end
end
end
endActive Support
You can test delegation based on the delegate method in the Active Support gem.
require 'spec_helper'
require 'active_support/core_ext/module/delegation'
module ActiveSupportDelegation
# rubocop:disable Style/ClassVars
class Post
attr_accessor :author
@@authors = ['Ann Rand', 'Catherine Asaro']
GENRES = ['Fiction', 'Science Fiction']
delegate :name, to: :author, allow_nil: true
delegate :name, to: :author, prefix: true
delegate :name, to: :author, prefix: :writer
delegate :name, to: :class, prefix: true
delegate :count, to: :@@authors
delegate :first, to: :GENRES
delegate :length, to: :'author.name', prefix: :name
def initialize
@author = Author.new
end
def inspect
'post'
end
end
class Author
def initialize
@name = 'Catherine Asaro'
end
def name(*)
@name
end
end
describe Post do
let(:author) { subject.author }
it { expect(subject.name).to eq 'Catherine Asaro' }
it { should delegate(:name).to(author) }
it { should delegate(:name).to(:@author) }
it { should delegate(:name).to(:author) }
it { should delegate(:name).to(:author).allow_nil }
it { should delegate(:name).to(:author).with_prefix }
it { should delegate(:name).to(:author).with_prefix(:writer) }
it { should delegate(:name).to(:author).with_block }
it { should delegate(:name).to(:author).with('Ms.') }
it { should delegate(:name).to(:author).with('Ms.').with_block }
it { should delegate(:name).to(:class).with_prefix }
it { should delegate(:count).to(:@@authors) }
it { should delegate(:first).to(:GENRES) }
it { should delegate(:name_length).to(:'author.name').as(:length) }
it { should delegate(:length).to(:'author.name').with_prefix(:name) }
end
endHowever, don't use the following features as they are not supported by the delegate method:
- delegation to objects
Forwardable Module
You can test delegation based on the Forwardable module.
require 'spec_helper'
module ForwardableDelegation
class Post
extend Forwardable
attr_accessor :author
def initialize
@author = Author.new
end
def_delegator :author, :name
def_delegator :author, :name, :author_name
def_delegator :author, :name, :writer
def_delegator :'author.name', :length, :name_length
end
class Author
def initialize
@name = 'Catherine Asaro'
end
def name(*)
@name
end
end
describe Post do
let(:author) { subject.author }
it { expect(subject.name).to eq 'Catherine Asaro' }
it { should delegate(:name).to(author) }
it { should delegate(:name).to(:@author) }
it { should delegate(:name).to(:author) }
it { should delegate(:name).to(:author).with('Ms.') }
it { should delegate(:name).to(:author).with_block }
it { should delegate(:name).to(:author).with('Ms.').with_block }
it { should delegate(:name).to(:author).with_prefix }
it { should delegate(:writer).to(:author).as(:name) }
it { should delegate(:name_length).to(:'author.name').as(:length) }
it { should delegate(:length).to(:'author.name').with_prefix(:name) }
end
endHowever, don't use the following features as they are not supported by the Forwardable module:
allow_nil- delegation to class variables
- delegation to constants
- delegation to objects
Contributing
- Fork it ( https://github.com/dwhelan/delegate_matcher/fork )
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create a new Pull Request
Notes
This matcher was inspired by Alan Winograd via the gist https://gist.github.com/awinograd/6158961