Skip to content

Commit 6f4de5a

Browse files
authored
Matcher delegate_method supports 'private: true' (#1653)
* Specify desired state in unit spec * Implement delegate_method supporting with_private
1 parent e8f81e3 commit 6f4de5a

File tree

2 files changed

+207
-5
lines changed

2 files changed

+207
-5
lines changed

lib/shoulda/matchers/independent/delegate_method_matcher.rb

Lines changed: 86 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -168,8 +168,27 @@ module Independent
168168
# should delegate_method(:plan).to(:subscription).allow_nil
169169
# end
170170
#
171-
# @return [DelegateMethodMatcher]
171+
# ##### with_private
172+
#
173+
# Use `with_private` if the delegation accounts for the fact that your
174+
# delegation is private. (This is mostly intended as an analogue to
175+
# the `private` option that Rails' `delegate` helper takes.)
176+
#
177+
# class Account
178+
# delegate :plan, to: :subscription, private: true
179+
# end
180+
#
181+
# # RSpec
182+
# describe Account do
183+
# it { should delegate_method(:plan).to(:subscription).with_private }
184+
# end
185+
#
186+
# # Minitest
187+
# class PageTest < Minitest::Test
188+
# should delegate_method(:plan).to(:subscription).with_private
189+
# end
172190
#
191+
# @return [DelegateMethodMatcher]
173192
def delegate_method(delegating_method)
174193
DelegateMethodMatcher.new(delegating_method).in_context(self)
175194
end
@@ -187,6 +206,7 @@ def initialize(delegating_method)
187206
@delegate_object_reader_method = nil
188207
@delegated_arguments = []
189208
@expects_to_allow_nil_delegate_object = false
209+
@expects_private_delegation = false
190210
end
191211

192212
def in_context(context)
@@ -202,14 +222,19 @@ def matches?(subject)
202222
subject_has_delegating_method? &&
203223
subject_has_delegate_object_reader_method? &&
204224
subject_delegates_to_delegate_object_correctly? &&
205-
subject_handles_nil_delegate_object?
225+
subject_handles_nil_delegate_object? &&
226+
subject_handles_private_delegation?
206227
end
207228

208229
def description
209230
string =
210231
"delegate #{formatted_delegating_method_name} to the " +
211232
"#{formatted_delegate_object_reader_method_name} object"
212233

234+
if expects_private_delegation?
235+
string << ' privately'
236+
end
237+
213238
if delegated_arguments.any?
214239
string << " passing arguments #{delegated_arguments.inspect}"
215240
end
@@ -254,6 +279,11 @@ def allow_nil
254279
self
255280
end
256281

282+
def with_private
283+
@expects_private_delegation = true
284+
self
285+
end
286+
257287
def build_delegating_method_prefix(prefix)
258288
case prefix
259289
when true, nil then delegate_object_reader_method
@@ -264,14 +294,19 @@ def build_delegating_method_prefix(prefix)
264294
def failure_message
265295
message = "Expected #{class_under_test} to #{description}.\n\n"
266296

267-
if failed_to_allow_nil_delegate_object?
297+
if failed_to_allow_nil_delegate_object? || failed_to_handle_private_delegation?
268298
message << formatted_delegating_method_name(include_module: true)
269299
message << ' did delegate to '
270300
message << formatted_delegate_object_reader_method_name
301+
end
302+
303+
if failed_to_allow_nil_delegate_object?
271304
message << ' when it was non-nil, but it failed to account '
272305
message << 'for when '
273306
message << formatted_delegate_object_reader_method_name
274307
message << ' *was* nil.'
308+
elsif failed_to_handle_private_delegation?
309+
message << ", but 'private: true' is missing."
275310
else
276311
message << 'Method calls sent to '
277312
message << formatted_delegate_object_reader_method_name(
@@ -322,6 +357,10 @@ def expects_to_allow_nil_delegate_object?
322357
@expects_to_allow_nil_delegate_object
323358
end
324359

360+
def expects_private_delegation?
361+
@expects_private_delegation
362+
end
363+
325364
def formatted_delegate_method(options = {})
326365
formatted_method_name_for(delegate_method, options)
327366
end
@@ -367,7 +406,11 @@ def delegate_object_received_call_with_delegated_arguments?
367406
end
368407

369408
def subject_has_delegating_method?
370-
subject.respond_to?(delegating_method)
409+
if expects_private_delegation?
410+
!subject.respond_to?(delegating_method) && subject.respond_to?(delegating_method, true)
411+
else
412+
subject.respond_to?(delegating_method)
413+
end
371414
end
372415

373416
def subject_has_delegate_object_reader_method?
@@ -381,7 +424,11 @@ def ensure_delegate_object_has_been_specified!
381424
end
382425

383426
def subject_delegates_to_delegate_object_correctly?
384-
call_delegating_method_with_delegate_method_returning(delegate_object)
427+
if expects_private_delegation?
428+
privately_call_delegating_method_with_delegate_method_returning(delegate_object)
429+
else
430+
call_delegating_method_with_delegate_method_returning(delegate_object)
431+
end
385432

386433
if delegated_arguments.any?
387434
delegate_object_received_call_with_delegated_arguments?
@@ -411,11 +458,37 @@ def subject_handles_nil_delegate_object?
411458
end
412459
end
413460

461+
def subject_handles_private_delegation?
462+
@subject_handled_private_delegation =
463+
if expects_private_delegation?
464+
begin
465+
call_delegating_method_with_delegate_method_returning(delegate_object)
466+
true
467+
rescue Module::DelegationError
468+
false
469+
rescue NoMethodError => e
470+
if e.message =~
471+
/private method `#{delegating_method}' called for/
472+
true
473+
else
474+
raise e
475+
end
476+
end
477+
else
478+
true
479+
end
480+
end
481+
414482
def failed_to_allow_nil_delegate_object?
415483
expects_to_allow_nil_delegate_object? &&
416484
!@subject_handled_nil_delegate_object
417485
end
418486

487+
def failed_to_handle_private_delegation?
488+
expects_private_delegation? &&
489+
!@subject_handled_private_delegation
490+
end
491+
419492
def call_delegating_method_with_delegate_method_returning(value)
420493
register_subject_double_collection_to(value)
421494

@@ -424,6 +497,14 @@ def call_delegating_method_with_delegate_method_returning(value)
424497
end
425498
end
426499

500+
def privately_call_delegating_method_with_delegate_method_returning(value)
501+
register_subject_double_collection_to(value)
502+
503+
Doublespeak.with_doubles_activated do
504+
subject.__send__(delegating_method, *delegated_arguments)
505+
end
506+
end
507+
427508
def register_subject_double_collection_to(returned_value)
428509
double_collection =
429510
Doublespeak.double_collection_for(subject.singleton_class)

spec/unit/shoulda/matchers/independent/delegate_method_matcher_spec.rb

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,15 @@ def country
102102
end
103103
end
104104
end
105+
106+
context 'qualified with #with_private' do
107+
it 'states that it should delegate method to the right object with right argument and makes is private' do
108+
matcher = delegate_method(:method_name).to(:delegate).with_private
109+
message = 'delegate #method_name to the #delegate object privately'
110+
111+
expect(matcher.description).to eq message
112+
end
113+
end
105114
end
106115

107116
context 'when the subject is a class' do
@@ -655,4 +664,116 @@ def hello
655664
end
656665
end
657666
end
667+
668+
context 'qualified with #with_private' do
669+
context 'when using delegate from Rails' do
670+
context 'when delegations were defined with :private' do
671+
it 'accepts' do
672+
define_class('Person') do
673+
delegate :hello, to: :country, private: true
674+
def country
675+
end
676+
end
677+
678+
person = Person.new
679+
680+
expect(person).to delegate_method(:hello).to(:country).with_private
681+
end
682+
end
683+
684+
context 'when delegations were not defined with :private' do
685+
it 'rejects with the correct failure message' do
686+
define_class('Person') do
687+
delegate :hello, to: :country
688+
def country
689+
end
690+
end
691+
692+
person = Person.new
693+
694+
message = <<-MESSAGE
695+
Expected Person to delegate #hello to the #country object privately.
696+
697+
Person#hello did delegate to #country, but 'private: true' is missing.
698+
MESSAGE
699+
700+
expectation = lambda do
701+
expect(person).to delegate_method(:hello).to(:country).with_private
702+
end
703+
704+
expect(&expectation).to fail_with_message(message)
705+
end
706+
707+
context 'with :prefix' do
708+
it 'accepts' do
709+
define_class('Person') do
710+
delegate :hello, to: :country, private: true, prefix: :user
711+
def country
712+
end
713+
end
714+
715+
person = Person.new
716+
717+
expect(person).to delegate_method(:hello).to(:country).with_prefix(:user).with_private
718+
end
719+
end
720+
721+
context 'with :as' do
722+
it 'accepts' do
723+
define_class('Company') do
724+
def name
725+
'Acme Company'
726+
end
727+
end
728+
729+
define_class('Person') do
730+
private
731+
732+
def company_name
733+
company.name
734+
end
735+
736+
def company
737+
Company.new
738+
end
739+
end
740+
741+
person = Person.new
742+
matcher = delegate_method(:company_name).to(:company).as(:name).with_private
743+
matcher.matches?(person)
744+
745+
expect(person.send(:company).name).to eq 'Acme Company'
746+
end
747+
748+
context 'and :prefix' do
749+
it 'accepts' do
750+
define_class('Company') do
751+
def name
752+
'Acme Company'
753+
end
754+
end
755+
756+
define_class('Person') do
757+
private
758+
759+
def company_name
760+
company.name
761+
end
762+
763+
def company
764+
Company.new
765+
end
766+
end
767+
768+
person = Person.new
769+
matcher = delegate_method(:company_name).to(:company).with_prefix(:user).as(:name).with_private
770+
matcher.matches?(person)
771+
772+
expect(person.send(:company).name).to eq 'Acme Company'
773+
end
774+
end
775+
end
776+
end
777+
end
778+
end
658779
end

0 commit comments

Comments
 (0)