rspec-sleeping_king_studios 2.6.0 → 2.7.0.rc.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,260 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ require 'sleeping_king_studios/tools/toolbelt'
6
+
7
+ require 'rspec/sleeping_king_studios/concerns'
8
+
9
+ module RSpec::SleepingKingStudios::Concerns
10
+ # Defines helpers for including reusable contracts in RSpec example groups.
11
+ #
12
+ # RSpec contracts are a mechanism for sharing tests between projects. For
13
+ # example, one library may define an interface or specification for a type of
14
+ # object, while a second library implements that object. By defining a
15
+ # contract and sharing that contract as part of the library, the developer
16
+ # ensures that any object that matches the contract has correctly implemented
17
+ # and conforms to the interface. This reduces duplication of tests and
18
+ # provides resiliency as an interface is developed over time and across
19
+ # versions of the library.
20
+ #
21
+ # Mechanically speaking, each contract encapsulates a section of RSpec code.
22
+ # When the contract is included in a spec, that code is then injected into the
23
+ # spec. Writing a contract, therefore, is no different than writing any other
24
+ # RSpec specification - it is only the delivery mechanism that differs. A
25
+ # contract can be any object that responds to #to_proc; the simplest contract
26
+ # is therefore a Proc or lambda that contains some RSpec code.
27
+ #
28
+ # @example Defining A Contract
29
+ # module ExampleContracts
30
+ # # This contract asserts that the object has the Enumerable module as an
31
+ # # ancestor, and that it responds to the #each method.
32
+ # SHOULD_BE_ENUMERABLE_CONTRACT = lambda do
33
+ # it 'should be Enumerable' do
34
+ # expect(subject).to be_a Enumerable
35
+ # end
36
+ #
37
+ # it 'should respond to #each' do
38
+ # expect(subject).to respond_to(:each).with(0).arguments
39
+ # end
40
+ # end
41
+ # end
42
+ #
43
+ # RSpec.describe Array do
44
+ # extend RSpec::SleepingKingStudios::Concerns::IncludeContract
45
+ #
46
+ # include_contract ExampleContracts::SHOULD_BE_ENUMERABLE_CONTRACT
47
+ # end
48
+ #
49
+ # RSpec.describe Hash do
50
+ # extend RSpec::SleepingKingStudios::Concerns::IncludeContract
51
+ # include ExampleContracts
52
+ #
53
+ # include_contract 'should be enumerable'
54
+ # end
55
+ #
56
+ # @example Defining A Contract With Parameters
57
+ # module SerializerContracts
58
+ # # This contract asserts that the serialized result has the expected
59
+ # # values.
60
+ # #
61
+ # # First, we pass the contract a series of attribute names. These are
62
+ # # used to assert that the serialized attributes match the values on the
63
+ # # original object.
64
+ # #
65
+ # # Second, we pass the contract a set of attribute names and values.
66
+ # # These are used to assert that the serialized attributes have the
67
+ # # specified values.
68
+ # #
69
+ # # Finally, we can pass the contract a block, which the contract then
70
+ # # executes. Note that the block is executed in the context of our
71
+ # # describe block, and thus can take advantage of our memoized
72
+ # # #serialized helper method.
73
+ # SHOULD_SERIALIZE_ATTRIBUTES_CONTRACT = lambda \
74
+ # do |*attributes, **values, &block|
75
+ # describe '#serialize' do
76
+ # let(:serialized) { subject.serialize }
77
+ #
78
+ # it { expect(subject).to respond_to(:serialize).with(0).arguments }
79
+ #
80
+ # attributes.each do |attribute|
81
+ # it "should serialize #{attribute}" do
82
+ # expect(serialized[attribute]).to be == subject[attribute]
83
+ # end
84
+ # end
85
+ #
86
+ # values.each do |attribute, value|
87
+ # it "should serialize #{attribute}" do
88
+ # expect(serialized[attribute]).to be == value
89
+ # end
90
+ # end
91
+ #
92
+ # instance_exec(&block) if block
93
+ # end
94
+ # end
95
+ #
96
+ # RSpec.describe CaptainPicard do
97
+ # extend RSpec::SleepingKingStudios::Concerns::IncludeContract
98
+ # include SerializerContracts
99
+ #
100
+ # include_contract 'should serialize attributes',
101
+ # :name,
102
+ # :rank,
103
+ # lights: 4 do
104
+ # it 'should serialize the catchphrase' do
105
+ # expect(serialized[:catchphrase]).to be == 'Make it so.'
106
+ # end
107
+ # end
108
+ # end
109
+ #
110
+ # @see RSpec::SleepingKingStudios::Contract.
111
+ module IncludeContract
112
+ class << self
113
+ # @private
114
+ def define_contract_method(context:, contract:, name:)
115
+ method_name = +'rspec_include_contract'
116
+ method_name << '_' << tools.str.underscore(name) if contract_name?(name)
117
+ method_name << '_' << tools.str.underscore(SecureRandom.uuid)
118
+ method_name = method_name.tr(' ', '_').intern
119
+
120
+ context.define_singleton_method(method_name, &contract)
121
+
122
+ yield method_name
123
+ ensure
124
+ if context.singleton_class.respond_to?(method_name)
125
+ context.singleton_class.remove_method(method_name)
126
+ end
127
+ end
128
+
129
+ # @private
130
+ def resolve_contract(context:, contract_or_name:)
131
+ validate_contract!(contract_or_name)
132
+
133
+ return contract_or_name unless contract_name?(contract_or_name)
134
+
135
+ contract_name = contract_or_name.to_s
136
+ contract =
137
+ resolve_contract_class(context, "#{contract_name} contract") ||
138
+ resolve_contract_const(context, "#{contract_name} contract") ||
139
+ resolve_contract_class(context, contract_name) ||
140
+ resolve_contract_const(context, contract_name)
141
+
142
+ return contract if contract
143
+
144
+ raise ArgumentError, "undefined contract #{contract_or_name.inspect}"
145
+ end
146
+
147
+ private
148
+
149
+ def contract?(contract_or_name)
150
+ contract_or_name.respond_to?(:to_proc)
151
+ end
152
+
153
+ def contract_name?(contract_or_name)
154
+ contract_or_name.is_a?(String) || contract_or_name.is_a?(Symbol)
155
+ end
156
+
157
+ def resolve_contract_class(context, contract_name)
158
+ class_name = tools.str.camelize(contract_name.tr(' ', '_'))
159
+
160
+ return nil unless context.const_defined?(class_name)
161
+
162
+ context.const_get(class_name)
163
+ end
164
+
165
+ def resolve_contract_const(context, contract_name)
166
+ const_name = tools.str.underscore(contract_name.tr(' ', '_')).upcase
167
+
168
+ return nil unless context.const_defined?(const_name)
169
+
170
+ context.const_get(const_name)
171
+ end
172
+
173
+ def tools
174
+ SleepingKingStudios::Tools::Toolbelt.instance
175
+ end
176
+
177
+ def validate_contract!(contract_or_name)
178
+ raise ArgumentError, "contract can't be blank" if contract_or_name.nil?
179
+
180
+ if contract_name?(contract_or_name)
181
+ return unless contract_or_name.to_s.empty?
182
+
183
+ raise ArgumentError, "contract can't be blank"
184
+ end
185
+
186
+ return if contract?(contract_or_name)
187
+
188
+ raise ArgumentError, 'contract must be a name or respond to #to_proc'
189
+ end
190
+ end
191
+
192
+ # As #include_contract, but wraps the contract in a focused example group.
193
+ #
194
+ # @see include_contract.
195
+ def finclude_contract(contract_or_name, *arguments, **keywords, &block)
196
+ fdescribe '(focused)' do
197
+ if keywords.empty?
198
+ include_contract(contract_or_name, *arguments, &block)
199
+ else
200
+ include_contract(contract_or_name, *arguments, **keywords, &block)
201
+ end
202
+ end
203
+ end
204
+
205
+ # Adds the contract to the example group with the given parameters.
206
+ #
207
+ # @overload include_contract(contract, *arguments, **keywords, &block)
208
+ # @param contract [#to_proc] The contract to include.
209
+ # @param arguments [Array] The arguments to pass to the contract.
210
+ # @param keywords [Hash] The keywords to pass to the contract.
211
+ #
212
+ # @yield A block passed to the contract.
213
+ #
214
+ # @overload include_contract(contract_name, *arguments, **keywords, &block)
215
+ # @param contract_name [String, Symbol] The name of contract to include.
216
+ # The contract must be defined as a Class or constant in the same scope,
217
+ # e.g. include_contract('does something') expects the example group to
218
+ # define either a DoSomething class or a DO_SOMETHING constant. The name
219
+ # can optionally be suffixed with "contract", so it will also match a
220
+ # DoSomethingContract class or a DO_SOMETHING_CONTRACT constant.
221
+ # @param arguments [Array] The arguments to pass to the contract.
222
+ # @param keywords [Hash] The keywords to pass to the contract.
223
+ #
224
+ # @yield A block passed to the contract.
225
+ #
226
+ # @raise ArgumentError
227
+ def include_contract(contract_or_name, *arguments, **keywords, &block) # rubocop:disable Metrics/MethodLength
228
+ concern = RSpec::SleepingKingStudios::Concerns::IncludeContract
229
+ contract = concern.resolve_contract(
230
+ context: self,
231
+ contract_or_name: contract_or_name
232
+ )
233
+
234
+ concern.define_contract_method(
235
+ context: self,
236
+ contract: contract,
237
+ name: contract_or_name
238
+ ) do |method_name|
239
+ if keywords.empty?
240
+ send(method_name, *arguments, &block)
241
+ else
242
+ send(method_name, *arguments, **keywords, &block)
243
+ end
244
+ end
245
+ end
246
+
247
+ # As #include_contract, but wraps the contract in a skipped example group.
248
+ #
249
+ # @see include_contract.
250
+ def xinclude_contract(contract_or_name, *arguments, **keywords, &block)
251
+ xdescribe '(skipped)' do
252
+ if keywords.empty?
253
+ include_contract(contract_or_name, *arguments, &block)
254
+ else
255
+ include_contract(contract_or_name, *arguments, **keywords, &block)
256
+ end
257
+ end
258
+ end
259
+ end
260
+ end
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec/sleeping_king_studios'
4
+ require 'rspec/sleeping_king_studios/concerns/include_contract'
5
+
6
+ module RSpec::SleepingKingStudios
7
+ # A Contract wraps RSpec functionality for sharing and reusability.
8
+ #
9
+ # An RSpec::SleepingKingStudios::Contract integrates with the
10
+ # .include_contract class method to share and reuse RSpec examples and
11
+ # configuration. The major advantage a Contract object provides over using a
12
+ # Proc is documentation - tools such as YARD do not gracefully handle bare
13
+ # lambdas, while the functionality and requirements of a Contract can be
14
+ # specified using standard patterns, such as documenting the parameters passed
15
+ # to a contract using the #apply method.
16
+ #
17
+ # @example Defining A Contract
18
+ # module ExampleContracts
19
+ # # This contract asserts that the object has the Enumerable module as an
20
+ # # ancestor, and that it responds to the #each method.
21
+ # class ShouldBeEnumerableContract
22
+ # extend RSpec::SleepingKingStudios::Contract
23
+ #
24
+ # # @!method apply(example_group)
25
+ # # Adds the contract to the example group.
26
+ #
27
+ # contract do
28
+ # it 'should be Enumerable' do
29
+ # expect(subject).to be_a Enumerable
30
+ # end
31
+ #
32
+ # it 'should respond to #each' do
33
+ # expect(subject).to respond_to(:each).with(0).arguments
34
+ # end
35
+ # end
36
+ # end
37
+ # end
38
+ #
39
+ # RSpec.describe Array do
40
+ # ExampleContracts::SHOULD_BE_ENUMERABLE_CONTRACT.apply(self)
41
+ # end
42
+ #
43
+ # RSpec.describe Hash do
44
+ # extend RSpec::SleepingKingStudios::Concerns::IncludeContract
45
+ # include ExampleContracts
46
+ #
47
+ # include_contract 'should be enumerable'
48
+ # end
49
+ #
50
+ # @example Defining A Contract With Parameters
51
+ # module SerializerContracts
52
+ # # This contract asserts that the serialized result has the expected
53
+ # # values.
54
+ # #
55
+ # # First, we pass the contract a series of attribute names. These are
56
+ # # used to assert that the serialized attributes match the values on the
57
+ # # original object.
58
+ # #
59
+ # # Second, we pass the contract a set of attribute names and values.
60
+ # # These are used to assert that the serialized attributes have the
61
+ # # specified values.
62
+ # #
63
+ # # Finally, we can pass the contract a block, which the contract then
64
+ # # executes. Note that the block is executed in the context of our
65
+ # # describe block, and thus can take advantage of our memoized
66
+ # # #serialized helper method.
67
+ # class ShouldSerializeAttributesContract
68
+ # extend RSpec::SleepingKingStudios::Contract
69
+ #
70
+ # contract do |*attributes, **values, &block|
71
+ # describe '#serialize' do
72
+ # let(:serialized) { subject.serialize }
73
+ #
74
+ # it { expect(subject).to respond_to(:serialize).with(0).arguments }
75
+ #
76
+ # attributes.each do |attribute|
77
+ # it "should serialize #{attribute}" do
78
+ # expect(serialized[attribute]).to be == subject[attribute]
79
+ # end
80
+ # end
81
+ #
82
+ # values.each do |attribute, value|
83
+ # it "should serialize #{attribute}" do
84
+ # expect(serialized[attribute]).to be == value
85
+ # end
86
+ # end
87
+ #
88
+ # instance_exec(&block) if block
89
+ # end
90
+ # end
91
+ # end
92
+ #
93
+ # RSpec.describe CaptainPicard do
94
+ # SerializerContracts::ShouldSerializeAttributesContract.apply(
95
+ # self,
96
+ # :name,
97
+ # :rank,
98
+ # lights: 4) \
99
+ # do
100
+ # it 'should serialize the catchphrase' do
101
+ # expect(serialized[:catchphrase]).to be == 'Make it so.'
102
+ # end
103
+ # end
104
+ # end
105
+ #
106
+ # @see RSpec::SleepingKingStudios::Concerns::IncludeContract.
107
+ module Contract
108
+ # Adds the contract to the given example group.
109
+ #
110
+ # @param example_group [RSpec::Core::ExampleGroup] The example group to
111
+ # which the contract is applied.
112
+ # @param arguments [Array] Optional arguments to pass to the contract.
113
+ # @param keywords [Hash] Optional keywords to pass to the contract.
114
+ #
115
+ # @yield A block to pass to the contract.
116
+ #
117
+ # @see #to_proc
118
+ def apply(example_group, *arguments, **keywords, &block)
119
+ concern = RSpec::SleepingKingStudios::Concerns::IncludeContract
120
+
121
+ concern.define_contract_method(
122
+ context: example_group,
123
+ contract: self,
124
+ name: tools.str.underscore(name).gsub('::', '_')
125
+ ) do |method_name|
126
+ if keywords.empty?
127
+ example_group.send(method_name, *arguments, &block)
128
+ else
129
+ example_group.send(method_name, *arguments, **keywords, &block)
130
+ end
131
+ end
132
+ end
133
+
134
+ # @overload contract()
135
+ # @return [Proc, nil] the contract implementation for the class.
136
+ #
137
+ # @overload contract()
138
+ # Sets the contract implementation for the class.
139
+ #
140
+ # @yield [*arguments, **keywords, &block] The implementation to
141
+ # configure for the class.
142
+ #
143
+ # @yieldparam arguments [Array] Optional arguments to pass to the
144
+ # contract.
145
+ # @yieldparam keywords [Hash] Optional keywords defined for the
146
+ # contract.
147
+ # @yieldparam block [Array] A block to pass to the contract.
148
+ def contract(&block)
149
+ return @contract = block if block_given?
150
+
151
+ @contract
152
+ end
153
+
154
+ # @return [Proc, nil] the contract implementation for the class.
155
+ def to_proc
156
+ @contract
157
+ end
158
+
159
+ private
160
+
161
+ def tools
162
+ SleepingKingStudios::Tools::Toolbelt.instance
163
+ end
164
+ end
165
+ end
@@ -1,11 +1,17 @@
1
1
  # lib/rspec/sleeping_king_studios/matchers/core/alias_method.rb
2
2
 
3
- require 'rspec/sleeping_king_studios/matchers/core/alias_method_matcher'
3
+ require 'rspec/sleeping_king_studios/matchers/core/have_aliased_method_matcher'
4
4
  require 'rspec/sleeping_king_studios/matchers/macros'
5
5
 
6
6
  module RSpec::SleepingKingStudios::Matchers::Macros
7
7
  # @see RSpec::SleepingKingStudios::Matchers::Core::AliasMethodMatcher#matches?
8
8
  def alias_method expected
9
- RSpec::SleepingKingStudios::Matchers::Core::AliasMethodMatcher.new expected
9
+ SleepingKingStudios::Tools::CoreTools.deprecate(
10
+ '#alias_method',
11
+ message: 'Use #have_aliased_method instead.'
12
+ )
13
+
14
+ RSpec::SleepingKingStudios::Matchers::Core::HaveAliasedMethodMatcher
15
+ .new expected
10
16
  end # method be_boolean
11
17
  end # module
@@ -1,107 +1,22 @@
1
- # lib/rspec/sleeping_king_studios/matchers/core/alias_method_matcher.rb
1
+ # frozen_string_literal: true
2
2
 
3
- require 'rspec/sleeping_king_studios/matchers/base_matcher'
4
3
  require 'rspec/sleeping_king_studios/matchers/core'
4
+ require 'rspec/sleeping_king_studios/matchers/core/have_aliased_method_matcher'
5
5
 
6
6
  module RSpec::SleepingKingStudios::Matchers::Core
7
7
  # Matcher for testing whether an object aliases a specified method using the
8
8
  # specified other method name.
9
9
  #
10
10
  # @since 2.2.0
11
- class AliasMethodMatcher < RSpec::SleepingKingStudios::Matchers::BaseMatcher
12
- # @param [String, Symbol] expected The name of the method that is expected
13
- # to be aliased.
14
- def initialize expected
15
- @old_method_name = @expected = expected.intern
16
- @errors = {}
17
- end # method initialize
18
-
19
- # Specifies the name of the new method.
20
- #
21
- # @param [String, Symbol] new_method_name The method name.
22
- #
23
- # @return [AliasMethodMatcher] self
24
- def as new_method_name
25
- @new_method_name = new_method_name
26
-
27
- self
28
- end # method as
29
-
30
- # (see BaseMatcher#description)
31
- def description
32
- str = "alias :#{old_method_name}"
33
-
34
- str << " as #{new_method_name.inspect}" if new_method_name
35
-
36
- str
37
- end # method description
38
-
39
- # (see BaseMatcher#failure_message)
40
- def failure_message
41
- message = "expected #{@actual.inspect} to alias :#{old_method_name}"
42
-
43
- message << " as #{new_method_name.inspect}" if new_method_name
44
-
45
- if @errors[:does_not_respond_to_old_method]
46
- message << ", but did not respond to :#{old_method_name}"
47
-
48
- return message
49
- end # if
50
-
51
- if @errors[:does_not_respond_to_new_method]
52
- message << ", but did not respond to :#{new_method_name}"
53
-
54
- return message
55
- end # if
56
-
57
- if @errors[:does_not_alias_method]
58
- message <<
59
- ", but :#{old_method_name} and :#{new_method_name} are different "\
60
- "methods"
61
-
62
- return message
63
- end # if
64
-
65
- message
66
- end # method failure_message
67
-
11
+ class AliasMethodMatcher < RSpec::SleepingKingStudios::Matchers::Core::HaveAliasedMethodMatcher
68
12
  # (see BaseMatcher#matches?)
69
- def matches? actual
70
- super
71
-
72
- raise ArgumentError.new('must specify a new method name') if new_method_name.nil?
73
-
74
- responds_to_methods? && aliases_method?
75
- end # method matches?
76
-
77
- private
78
-
79
- attr_reader :old_method_name, :new_method_name
80
-
81
- def aliases_method?
82
- unless @actual.method(old_method_name) == @actual.method(new_method_name)
83
- @errors[:does_not_alias_method] = true
13
+ def matches?(actual)
14
+ SleepingKingStudios::Tools::CoreTools.deprecate(
15
+ 'AliasMethodMatcher',
16
+ message: 'Use a HaveAliasedMethodMatcher instead.'
17
+ )
84
18
 
85
- return false
86
- end # unless
87
-
88
- true
89
- end # method aliases_method?
90
-
91
- def responds_to_methods?
92
- unless @actual.respond_to?(old_method_name)
93
- @errors[:does_not_respond_to_old_method] = true
94
-
95
- return false
96
- end # unless
97
-
98
- unless @actual.respond_to?(new_method_name)
99
- @errors[:does_not_respond_to_new_method] = true
100
-
101
- return false
102
- end # unless
103
-
104
- true
105
- end # method responds_to_methods?
106
- end # class
107
- end # module
19
+ super
20
+ end
21
+ end
22
+ end
@@ -125,7 +125,14 @@ module RSpec::SleepingKingStudios::Matchers::Core
125
125
 
126
126
  # (see BaseMatcher#matches?)
127
127
  def matches? actual
128
- SleepingKingStudios::Tools::CoreTools.deprecate('DelegateMethodMatcher')
128
+ # :nocov:
129
+ if RUBY_VERSION < '3.0'
130
+ SleepingKingStudios::Tools::CoreTools.deprecate('DelegateMethodMatcher')
131
+ else
132
+ SleepingKingStudios::Tools::CoreTools
133
+ .new(deprecation_strategy: 'raise')
134
+ .deprecate('DelegateMethodMatcher')
135
+ end
129
136
 
130
137
  super
131
138
 
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec/sleeping_king_studios/matchers/core/have_aliased_method_matcher'
4
+ require 'rspec/sleeping_king_studios/matchers/macros'
5
+
6
+ module RSpec::SleepingKingStudios::Matchers::Macros
7
+ # @see RSpec::SleepingKingStudios::Matchers::Core::HaveAliasedMethodMatcher#matches?
8
+ def have_aliased_method(original_name)
9
+ RSpec::SleepingKingStudios::Matchers::Core::HaveAliasedMethodMatcher
10
+ .new(original_name)
11
+ end
12
+ end