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:
$ bundle
Or install it yourself as:
$ gem install delegate_matcher
Then 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
end
Delegate 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
end
Note: 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
end
You 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
end
Handling 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
end
Nil 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.')
end
Also, 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')
end
You 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()
end
For 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(*)
end
Blocks
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
end
You 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? }
end
If 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 }
end
Multiple 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
end
Active 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
end
However, 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
end
However, 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