Skip to content

Commit

Permalink
Matcher delegate_method supports 'private: true' (#1653)
Browse files Browse the repository at this point in the history
* Specify desired state in unit spec

* Implement delegate_method supporting with_private
  • Loading branch information
pr0d1r2 authored Oct 8, 2024
1 parent e8f81e3 commit 6f4de5a
Show file tree
Hide file tree
Showing 2 changed files with 207 additions and 5 deletions.
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
121 changes: 121 additions & 0 deletions spec/unit/shoulda/matchers/independent/delegate_method_matcher_spec.rb
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

0 comments on commit 6f4de5a

Please sign in to comment.