No commit activity in last 3 years
No release in over 3 years
A matcher for testing ruby delegation.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

~> 1.7
~> 0.7
~> 2.13
~> 10.0
~> 3.0
~> 1.1
~> 0.30
~> 0.9

Runtime

 Project Readme

Gem Version Build Status Code Climate Coverage Status

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

  1. Fork it ( https://github.com/dwhelan/delegate_matcher/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

Notes

This matcher was inspired by Alan Winograd via the gist https://gist.github.com/awinograd/6158961