Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Matcher delegate_method supports 'private: true' #1653

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 86 additions & 5 deletions lib/shoulda/matchers/independent/delegate_method_matcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,27 @@ module Independent
# should delegate_method(:plan).to(:subscription).allow_nil
# end
#
# @return [DelegateMethodMatcher]
# ##### with_private
#
# Use `with_private` if the delegation accounts for the fact that your
# delegation is private. (This is mostly intended as an analogue to
# the `private` option that Rails' `delegate` helper takes.)
#
# class Account
# delegate :plan, to: :subscription, private: true
# end
#
# # RSpec
# describe Account do
# it { should delegate_method(:plan).to(:subscription).with_private }
# end
#
# # Minitest
# class PageTest < Minitest::Test
# should delegate_method(:plan).to(:subscription).with_private
# end
#
# @return [DelegateMethodMatcher]
def delegate_method(delegating_method)
DelegateMethodMatcher.new(delegating_method).in_context(self)
end
Expand All @@ -187,6 +206,7 @@ def initialize(delegating_method)
@delegate_object_reader_method = nil
@delegated_arguments = []
@expects_to_allow_nil_delegate_object = false
@expects_private_delegation = false
end

def in_context(context)
Expand All @@ -202,14 +222,19 @@ def matches?(subject)
subject_has_delegating_method? &&
subject_has_delegate_object_reader_method? &&
subject_delegates_to_delegate_object_correctly? &&
subject_handles_nil_delegate_object?
subject_handles_nil_delegate_object? &&
subject_handles_private_delegation?
end

def description
string =
"delegate #{formatted_delegating_method_name} to the " +
"#{formatted_delegate_object_reader_method_name} object"

if expects_private_delegation?
string << ' privately'
end

if delegated_arguments.any?
string << " passing arguments #{delegated_arguments.inspect}"
end
Expand Down Expand Up @@ -254,6 +279,11 @@ def allow_nil
self
end

def with_private
@expects_private_delegation = true
self
end

def build_delegating_method_prefix(prefix)
case prefix
when true, nil then delegate_object_reader_method
Expand All @@ -264,14 +294,19 @@ def build_delegating_method_prefix(prefix)
def failure_message
message = "Expected #{class_under_test} to #{description}.\n\n"

if failed_to_allow_nil_delegate_object?
if failed_to_allow_nil_delegate_object? || failed_to_handle_private_delegation?
message << formatted_delegating_method_name(include_module: true)
message << ' did delegate to '
message << formatted_delegate_object_reader_method_name
end

if failed_to_allow_nil_delegate_object?
message << ' when it was non-nil, but it failed to account '
message << 'for when '
message << formatted_delegate_object_reader_method_name
message << ' *was* nil.'
elsif failed_to_handle_private_delegation?
message << ", but 'private: true' is missing."
else
message << 'Method calls sent to '
message << formatted_delegate_object_reader_method_name(
Expand Down Expand Up @@ -322,6 +357,10 @@ def expects_to_allow_nil_delegate_object?
@expects_to_allow_nil_delegate_object
end

def expects_private_delegation?
@expects_private_delegation
end

def formatted_delegate_method(options = {})
formatted_method_name_for(delegate_method, options)
end
Expand Down Expand Up @@ -367,7 +406,11 @@ def delegate_object_received_call_with_delegated_arguments?
end

def subject_has_delegating_method?
subject.respond_to?(delegating_method)
if expects_private_delegation?
!subject.respond_to?(delegating_method) && subject.respond_to?(delegating_method, true)
else
subject.respond_to?(delegating_method)
end
end

def subject_has_delegate_object_reader_method?
Expand All @@ -381,7 +424,11 @@ def ensure_delegate_object_has_been_specified!
end

def subject_delegates_to_delegate_object_correctly?
call_delegating_method_with_delegate_method_returning(delegate_object)
if expects_private_delegation?
privately_call_delegating_method_with_delegate_method_returning(delegate_object)
else
call_delegating_method_with_delegate_method_returning(delegate_object)
end

if delegated_arguments.any?
delegate_object_received_call_with_delegated_arguments?
Expand Down Expand Up @@ -411,11 +458,37 @@ def subject_handles_nil_delegate_object?
end
end

def subject_handles_private_delegation?
@subject_handled_private_delegation =
if expects_private_delegation?
begin
call_delegating_method_with_delegate_method_returning(delegate_object)
true
rescue Module::DelegationError
false
rescue NoMethodError => e
if e.message =~
/private method `#{delegating_method}' called for/
true
else
raise e
end
end
else
true
end
end

def failed_to_allow_nil_delegate_object?
expects_to_allow_nil_delegate_object? &&
!@subject_handled_nil_delegate_object
end

def failed_to_handle_private_delegation?
expects_private_delegation? &&
!@subject_handled_private_delegation
end

def call_delegating_method_with_delegate_method_returning(value)
register_subject_double_collection_to(value)

Expand All @@ -424,6 +497,14 @@ def call_delegating_method_with_delegate_method_returning(value)
end
end

def privately_call_delegating_method_with_delegate_method_returning(value)
register_subject_double_collection_to(value)

Doublespeak.with_doubles_activated do
subject.__send__(delegating_method, *delegated_arguments)
end
end

def register_subject_double_collection_to(returned_value)
double_collection =
Doublespeak.double_collection_for(subject.singleton_class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,15 @@ def country
end
end
end

context 'qualified with #with_private' do
it 'states that it should delegate method to the right object with right argument and makes is private' do
matcher = delegate_method(:method_name).to(:delegate).with_private
message = 'delegate #method_name to the #delegate object privately'

expect(matcher.description).to eq message
end
end
end

context 'when the subject is a class' do
Expand Down Expand Up @@ -655,4 +664,116 @@ def hello
end
end
end

context 'qualified with #with_private' do
context 'when using delegate from Rails' do
context 'when delegations were defined with :private' do
it 'accepts' do
define_class('Person') do
delegate :hello, to: :country, private: true
def country
end
end

person = Person.new

expect(person).to delegate_method(:hello).to(:country).with_private
end
end

context 'when delegations were not defined with :private' do
it 'rejects with the correct failure message' do
define_class('Person') do
delegate :hello, to: :country
def country
end
end

person = Person.new

message = <<-MESSAGE
Expected Person to delegate #hello to the #country object privately.

Person#hello did delegate to #country, but 'private: true' is missing.
MESSAGE

expectation = lambda do
expect(person).to delegate_method(:hello).to(:country).with_private
end

expect(&expectation).to fail_with_message(message)
end

context 'with :prefix' do
it 'accepts' do
define_class('Person') do
delegate :hello, to: :country, private: true, prefix: :user
def country
end
end

person = Person.new

expect(person).to delegate_method(:hello).to(:country).with_prefix(:user).with_private
end
end

context 'with :as' do
it 'accepts' do
define_class('Company') do
def name
'Acme Company'
end
end

define_class('Person') do
private

def company_name
company.name
end

def company
Company.new
end
end

person = Person.new
matcher = delegate_method(:company_name).to(:company).as(:name).with_private
matcher.matches?(person)

expect(person.send(:company).name).to eq 'Acme Company'
end

context 'and :prefix' do
it 'accepts' do
define_class('Company') do
def name
'Acme Company'
end
end

define_class('Person') do
private

def company_name
company.name
end

def company
Company.new
end
end

person = Person.new
matcher = delegate_method(:company_name).to(:company).with_prefix(:user).as(:name).with_private
matcher.matches?(person)

expect(person.send(:company).name).to eq 'Acme Company'
end
end
end
end
end
end
end
Loading