rspec-sleeping_king_studios 2.7.0 → 2.8.0.rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -0
  3. data/README.md +228 -9
  4. data/config/rubocop-rspec.yml +41 -0
  5. data/lib/rspec/sleeping_king_studios/concerns/example_constants.rb +107 -74
  6. data/lib/rspec/sleeping_king_studios/concerns/memoized_helpers.rb +19 -0
  7. data/lib/rspec/sleeping_king_studios/concerns/shared_example_group.rb +5 -2
  8. data/lib/rspec/sleeping_king_studios/concerns.rb +8 -3
  9. data/lib/rspec/sleeping_king_studios/configuration.rb +45 -37
  10. data/lib/rspec/sleeping_king_studios/deferred/call.rb +74 -0
  11. data/lib/rspec/sleeping_king_studios/deferred/calls/example.rb +42 -0
  12. data/lib/rspec/sleeping_king_studios/deferred/calls/example_group.rb +42 -0
  13. data/lib/rspec/sleeping_king_studios/deferred/calls/hook.rb +64 -0
  14. data/lib/rspec/sleeping_king_studios/deferred/calls/included_examples.rb +34 -0
  15. data/lib/rspec/sleeping_king_studios/deferred/calls/shared_examples.rb +41 -0
  16. data/lib/rspec/sleeping_king_studios/deferred/calls.rb +19 -0
  17. data/lib/rspec/sleeping_king_studios/deferred/consumer.rb +159 -0
  18. data/lib/rspec/sleeping_king_studios/deferred/definitions.rb +42 -0
  19. data/lib/rspec/sleeping_king_studios/deferred/dependencies.rb +138 -0
  20. data/lib/rspec/sleeping_king_studios/deferred/dsl/example_constants.rb +72 -0
  21. data/lib/rspec/sleeping_king_studios/deferred/dsl/example_groups.rb +69 -0
  22. data/lib/rspec/sleeping_king_studios/deferred/dsl/examples.rb +84 -0
  23. data/lib/rspec/sleeping_king_studios/deferred/dsl/hooks.rb +125 -0
  24. data/lib/rspec/sleeping_king_studios/deferred/dsl/memoized_helpers.rb +123 -0
  25. data/lib/rspec/sleeping_king_studios/deferred/dsl/shared_examples.rb +128 -0
  26. data/lib/rspec/sleeping_king_studios/deferred/dsl.rb +26 -0
  27. data/lib/rspec/sleeping_king_studios/deferred/examples.rb +83 -0
  28. data/lib/rspec/sleeping_king_studios/deferred/missing.rb +46 -0
  29. data/lib/rspec/sleeping_king_studios/deferred/provider.rb +164 -0
  30. data/lib/rspec/sleeping_king_studios/deferred.rb +142 -0
  31. data/lib/rspec/sleeping_king_studios/matchers/built_in/include_matcher.rb +85 -70
  32. data/lib/rspec/sleeping_king_studios/matchers/core/deep_matcher.rb +28 -23
  33. data/lib/rspec/sleeping_king_studios/sandbox.rb +105 -0
  34. data/lib/rspec/sleeping_king_studios/version.rb +4 -3
  35. data/lib/rspec/sleeping_king_studios.rb +10 -4
  36. metadata +36 -141
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 54fafe35b632818612bf767e1a2ad05e9338fd4dd68a7356f4540473423c9888
4
- data.tar.gz: 6cfa48a9b350c0b19ac7fdd6a1d6a14a274b0a26ef873842b84d5e9d82438062
3
+ metadata.gz: f0fdf0b5b82bec68224de25df61b57af9d3910c48b30676fd96dafc0862ed5c3
4
+ data.tar.gz: 96e93e8c3438a94ab2f549cb5487ff6f3f1d3a72f8d935973eab796126eb5aa1
5
5
  SHA512:
6
- metadata.gz: f342e4dc730b32e64339aaac823350f8de678ec8d3820c9f64ea6a8037e29df4fc0d91e6e02cc8d13a8e0b1368003907c749a3801f64060a2669519933bd2b52
7
- data.tar.gz: 0f4f12cb9d5c1e350363759f2672bb9e96876f570b1bcc3aa4068171b47fcb6a752b51a9d8ca6ce184e172b3f9a1de0ef1b5bcc4e2cbbb80b6ccdef7f5761438
6
+ metadata.gz: f2d2c014fccc9f46abf5a1a45d14571b46e33f56edc6c3d7bb23326b8da15624ff02a24383f9da135d38a6c15fd39a171da5037c81d5d63dabeabadc68736699
7
+ data.tar.gz: b972588c34e3dbf36778330e3006e432c62c000823686186b1004a9b50a3b2ee4193b6d0f6d932f59a8daeb5eef7218bc5a45db4813b411d88b1a803842a6174
data/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.8.0
4
+
5
+ Updated minimum `Hashdiff` version to `~> 1.1`.
6
+
7
+ Extracted `rubocop-rspec` configuration. To add support for `RSpec::SleepingKingStudios` extensions to `rubocop-rspec`, add the following to your `.rubocop.yml` configuration file:
8
+
9
+ ```yml
10
+ inherit_gem:
11
+ rspec-sleeping_king_studios: 'config/rubocop-rspec.yml'
12
+ ```
13
+
14
+ ### Concerns
15
+
16
+ #### Memoized Helpers
17
+
18
+ Implemented the `let?` memoized helper, which defines a helper method if there is not already a method with the given name.
19
+
20
+ ### Deferred Examples
21
+
22
+ Implemented deferred examples, which provide an alternative implementation for sharing specifications between projects.
23
+
24
+ ### Sandbox
25
+
26
+ Implemented `RSpec::SleepingKingStudios::Sandbox` for running test files in an isolated environment.
27
+
3
28
  ## 2.7.0
4
29
 
5
30
  ### Concerns
data/README.md CHANGED
@@ -1,16 +1,32 @@
1
- # RSpec::SleepingKingStudios [![Build Status](https://travis-ci.org/sleepingkingstudios/rspec-sleeping_king_studios.svg?branch=master)](https://travis-ci.org/sleepingkingstudios/rspec-sleeping_king_studios)
1
+ # RSpec::SleepingKingStudios
2
2
 
3
3
  A collection of matchers and extensions to ease TDD/BDD using RSpec. Extends built-in matchers with new functionality, such as support for Ruby 2.0+ keyword arguments, and adds new matchers for testing boolean-ness, object reader/writer properties, object constructor arguments, ActiveModel validations, and more. Also defines shared example groups for more expressive testing.
4
4
 
5
+ <blockquote>
6
+ Read The
7
+ <a href="https://www.sleepingkingstudios.com/rspec-sleeping_king_studios" target="_blank">
8
+ Documentation
9
+ </a>
10
+ </blockquote>
11
+
5
12
  ## About
6
13
 
14
+ ### Setup
15
+
16
+ To add support for `RSpec::SleepingKingStudios` extensions to `rubocop-rspec`, add the following to your `.rubocop.yml` configuration file:
17
+
18
+ ```yml
19
+ inherit_gem:
20
+ rspec-sleeping_king_studios: 'config/rubocop-rspec.yml'
21
+ ```
22
+
7
23
  ### Compatibility
8
24
 
9
25
  RSpec::SleepingKingStudios is tested against the following dependencies:
10
26
 
11
- - Ruby (MRI) 2.7 through 3.1
12
- - RSpec versions 3.4 through 3.10
13
- - ActiveModel versions 3.0 through 6.1
27
+ - Ruby (MRI) 3.1 through 3.4
28
+ - RSpec versions 3.9 through 3.13
29
+ - ActiveModel versions 6.1 through 8.0
14
30
 
15
31
  ### Documentation
16
32
 
@@ -18,7 +34,7 @@ Documentation is generated using [YARD](https://yardoc.org/), and can be generat
18
34
 
19
35
  ### License
20
36
 
21
- Copyright (c) 2013-2021 Rob Smith
37
+ Copyright (c) 2013-2025 Rob Smith
22
38
 
23
39
  RSpec::SleepingKingStudios is released under the [MIT License](https://opensource.org/licenses/MIT).
24
40
 
@@ -34,7 +50,6 @@ To contribute code, please fork the repository, make the desired updates, and th
34
50
 
35
51
  Please note that the `RSpec::SleepingKingStudios` project is released with a [Contributor Code of Conduct](https://github.com/sleepingkingstudios/rspec-sleeping_king_studios/blob/master/CODE_OF_CONDUCT.md). By contributing to this project, you agree to abide by its terms.
36
52
 
37
-
38
53
  ## Configuration
39
54
 
40
55
  RSpec::SleepingKingStudios now has configuration options available through `RSpec.configuration`. For example, to set the behavior of the matcher examples when a failure message expectation is undefined (see RSpec Matcher Examples, below), put the following in your `spec_helper` or other configuration file:
@@ -302,6 +317,44 @@ A simplified syntax for re-using shared context or examples without having to ex
302
317
 
303
318
  (also `::xinclude_context`) A shortcut for skipping the example group by wrapping it in a `describe` block, similar to the built-in `xit` and `xdescribe` methods.
304
319
 
320
+ ### Memoized Helpers
321
+
322
+ ```ruby
323
+ require 'rspec/sleepingkingstudios/concerns/memoized_helpers'
324
+ ```
325
+
326
+ Methods for defining memoized helpers, similar to the core RSpec `let` method.
327
+
328
+ #### `.let?`
329
+
330
+ The `.let?` class method defines a memoized helper if and only if there is not an existing method with the same name defined in the example group. Among other use cases, this allows for defining default values in shared examples.
331
+
332
+ ```ruby
333
+ RSpec.describe Rocket do
334
+ subject(:rocket) { described_class.new(name: 'Imp IV') }
335
+
336
+ shared_examples 'should launch the rocket' do
337
+ let?(:launch_site) { 'KSC' }
338
+
339
+ it 'should launch the rocket' do
340
+ expect { rocket.launch_from(launch_site) }
341
+ .to change(rocket, :launched?)
342
+ .to be true
343
+ end
344
+ end
345
+
346
+ describe '#launch_from' do
347
+ include_examples 'should launch the rocket'
348
+
349
+ describe 'with launch_site: value' do
350
+ let(:launch_site) { 'Baikerbanur' }
351
+
352
+ include_examples 'should launch the rocket'
353
+ end
354
+ end
355
+ end
356
+ ```
357
+
305
358
  ### Shared Example Groups
306
359
 
307
360
  require 'rspec/sleeping_king_studios/concerns/shared_example_group'
@@ -439,7 +492,7 @@ A simplified syntax for re-using shared context or examples without having to ex
439
492
  ## Contracts
440
493
 
441
494
  ```ruby
442
- require 'rspec/sleepingkingstudios/contract'
495
+ require 'rspec/sleeping_king_studios/contract'
443
496
  ```
444
497
 
445
498
  An `RSpec::SleepingKingStudios::Contract` object encapsulates a partial RSpec specification. Unlike a traditional shared example group, a contract can be reused across projects, allowing a library to define an interface, provide a reference implementation, and publish tests that validate other implementations.
@@ -450,7 +503,9 @@ Contracts can be added to an example group either through the `.apply` method or
450
503
  module ExampleContracts
451
504
  # This contract asserts that the object has the Enumerable module as an
452
505
  # ancestor, and that it responds to the #each method.
453
- class ShouldBeEnumerableContract < RSpec::SleepingKingStudios::Contract
506
+ class ShouldBeEnumerableContract
507
+ extend RSpec::SleepingKingStudios::Contract
508
+
454
509
  # @!method apply(example_group)
455
510
  # Adds the contract to the example group.
456
511
 
@@ -786,7 +841,7 @@ When the value does not match the expectation, the failure message will provide
786
841
  # expected: == {:body=>{:order=>{:id=>an instance of Integer, :total=>"9.99"}}, :status=>200}
787
842
  # got: {:status=>400, :body=>{:order=>{:fulfilled=>false, :total=>"19.99"}}, :errors=>["Insufficient funds"]}
788
843
  #
789
- # (compared using HashDiff)
844
+ # (compared using Hashdiff)
790
845
  #
791
846
  # Diff:
792
847
  # + :body.:order.:fulfilled => got false
@@ -961,6 +1016,170 @@ Creates a value spy that watches the value of a method call or block. The spy al
961
1016
 
962
1017
  **Chaining:** None.
963
1018
 
1019
+ ## Sandbox
1020
+
1021
+ The `RSpec::SleepingKingStudios::Sandbox` module allows for running a spec file or files in an isolated environment and capturing the results. This can be useful for testing code meant to enhance your tests, such as a custom RSpec matcher or a shared example group.
1022
+
1023
+ First, define a spec file to run. As a recommended convention, spec files to be run in a sandbox should be given the `spec.fixture.rb` suffix to ensure they are not accidentally run with the main test suite.
1024
+
1025
+ ```ruby
1026
+ # frozen_string_literal: true
1027
+
1028
+ # In spec/rocket_spec.fixture.rb:
1029
+ RSpec.describe Rocket do
1030
+ describe '#launch' do
1031
+ it 'should launch the rocket' do
1032
+ expect { subject.launch }.to change(subject, :launched?).to be true
1033
+ end
1034
+ end
1035
+ end
1036
+ ```
1037
+
1038
+ Defining fixture files rather than generating temporary files is recommended for performance reasons, but both approaches are possible. Once the file is defined, it can be run in a sandbox:
1039
+
1040
+ ```ruby
1041
+ result = RSpec::SleepingKingStudios::Sandbox.run('spec/rocket_spec.fixture.rb')
1042
+
1043
+ result.class #=> RSpec::SleepingKingStudios::Sandbox::Result
1044
+ result.output #=> """
1045
+ # Rocket
1046
+ # #launch
1047
+ # should launch the rocket
1048
+ #
1049
+ # 1 example, 0 failures
1050
+ # """
1051
+ result.status #=> 1
1052
+ result.summary #=> 1 example, 0 failures
1053
+ result.example_descriptions #=> [
1054
+ # 'Rocket#launch should launch the rocket'
1055
+ # ]
1056
+ ```
1057
+
1058
+ The `.run` method returns an instance of `RSpec::SleepingKingStudios::Sandbox::Result`, which wraps the result of running the specified spec files. It defines the following methods:
1059
+
1060
+ - `#errors`: The output captured from STDERR when running the files.
1061
+ - `#example_descriptions`: The full description for each evaluated example.
1062
+ - `#json`: The json output from running the files.
1063
+ - `#output`: The output captured from STDOUT when running the files. The specs are run with the `--format=doc` flag, so this will include the individual examples as well as the summary.
1064
+ - `#summary`: The summary line for the tests.
1065
+
1066
+ ## Deferred Examples
1067
+
1068
+ `RSpec::SleepingKingStudios::Deferred` provides a mechanism for defining specifications that can be reused and shared between projects. For example, a library could use deferred examples to define an interface and test a reference implementation; projects that use that library could then use the published deferred examples to validate their own implementations of that interface.
1069
+
1070
+ At its core, a deferred example group is a Ruby `module`, and hooks into the inheritance hierarchy when `include`d into an example group. This gives certain advantages relative to traditional shared examples (and contracts):
1071
+
1072
+ - Deferred examples are defined in a specific context, and can be shared by simply `include`-ing the containing module (or the deferred examples directly). There is no global namespace, and using deferred examples (even from other projects) is as simple as an `include` call.
1073
+ - Deferred examples stack, rather than conflict. This means helper methods and memoized helpers can reference other included examples using `super()`, rather than simply overwriting other helpers at the same "level".
1074
+ - Unlike shared example groups, deferred examples defined via a `Provider` (see [Parameterized Examples](#parameterized-examples), below) can accept a block parameter.
1075
+
1076
+ ```ruby
1077
+ module ShouldBeAVehicleExamples
1078
+ include RSpec::SleepingKingStudios::Deferred::Examples
1079
+
1080
+ describe '#type' do
1081
+ it { expect(subject).to respond_to(:type).with(0).arguments }
1082
+
1083
+ it { expect(subject.type).to be == expected_type }
1084
+ end
1085
+ end
1086
+
1087
+ RSpec.describe Rocket do
1088
+ include ShouldBeAVehicleExamples
1089
+
1090
+ subject(:rocket) { described_class.new }
1091
+
1092
+ let(:expected_type) { :rocket }
1093
+
1094
+ describe '#launch' do
1095
+ it { expect(rocket).to respond_to(:launch) }
1096
+ end
1097
+ end
1098
+ ```
1099
+
1100
+ ### Parameterized Examples
1101
+
1102
+ Deferred examples can also be defined with parameters using the `Deferred::Provider` DSL. Defining a parameterized example group allows for defining and sharing specs that describe complex and conditional behavior.
1103
+
1104
+ For deferred specs that set up a context rather than examples, `deferred_context` is provided as an alias to `Provider.deferred_examples`.
1105
+
1106
+ ```ruby
1107
+ module VehicleExamples
1108
+ include RSpec::SleepingKingStudios::Deferred::Provider
1109
+
1110
+ deferred_context 'when the vehicle needs to be serviced' \
1111
+ do |serviced_at: '2020-01-01'|
1112
+ before(:example) do
1113
+ vehicle.last_serviced_at = serviced_at
1114
+ end
1115
+ end
1116
+
1117
+ deferred_examples 'should be a Vehicle' do |vehicle_type:|
1118
+ it { expect(subject).to be_a Spec::Models::Vehicle }
1119
+
1120
+ describe '#type' do
1121
+ it { expect(subject.type).to be == vehicle_type }
1122
+ end
1123
+ end
1124
+ end
1125
+ ```
1126
+
1127
+ The deferred examples can be included in example groups using the `Deferred::Consumer` DSL.
1128
+
1129
+ ```ruby
1130
+ RSpec.describe Car do
1131
+ include RSpec::SleepingKingStudios::Deferred::Consumer
1132
+ include VehicleExamples
1133
+
1134
+ subject(:car) { described_class.new }
1135
+
1136
+ include_deferred 'should be a Vehicle', vehicle_type: :car
1137
+ end
1138
+
1139
+ RSpec.describe Rocket do
1140
+ include RSpec::SleepingKingStudios::Deferred::Consumer
1141
+ include VehicleExamples
1142
+
1143
+ subject(:rocket) { described_class.new }
1144
+
1145
+ include_deferred 'should be a Vehicle', vehicle_type: :rocket
1146
+ end
1147
+ ```
1148
+
1149
+ `Deferred::Consumer` also defines support for wrapped deferred examples, which automatically generate a new context and include the deferred examples in the new example group. If `#wrap_deferred` is passed a block, that block will automatically be evaluated in the context of the example group, allowing you to define additional context or examples.
1150
+
1151
+ ```ruby
1152
+ RSpec.describe Car do
1153
+ include RSpec::SleepingKingStudios::Deferred::Consumer
1154
+ include VehicleExamples
1155
+
1156
+ subject(:car) { described_class.new }
1157
+
1158
+ wrap_deferred 'when the vehicle needs to be serviced' do
1159
+ it { expect(car.last_serviced_at).to be == '2020-01-01' }
1160
+ end
1161
+ end
1162
+ ```
1163
+
1164
+ `Deferred::Consumer` also includes the shortcuts `#finclude_deferred` and `#fwrap_deferred` to automatically focus deferred examples, or `#xinclude_deferred` and `#xwrap_deferred` to skip deferred examples.
1165
+
1166
+ The `defined_deferred_examples?` and `defined_deferred_context?` methods allow checking for the presence of a deferred example group.
1167
+
1168
+ ```ruby
1169
+ RSpec.describe Boat do
1170
+ include RSpec::SleepingKingStudios::Deferred::Consumer
1171
+ include VehicleExamples
1172
+
1173
+ subject(:boat) { described_class.new }
1174
+
1175
+ if defined_deferred_examples?('should be a water Vehicle')
1176
+ include_deferred 'should be a water Vehicle'
1177
+ else
1178
+ pending 'deferred examples "should be a water Vehicle" is not defined'
1179
+ end
1180
+ end
1181
+ ```
1182
+
964
1183
  ## Shared Examples
965
1184
 
966
1185
  To use a custom example group, `require` the associated file and then `include`
@@ -0,0 +1,41 @@
1
+ RSpec:
2
+ Language:
3
+ ExampleGroups:
4
+ Regular:
5
+ - context
6
+ - describe
7
+ - wrap_context
8
+ - wrap_deferred
9
+ Skipped:
10
+ - xcontext
11
+ - xdescribe
12
+ - xwrap_context
13
+ - xwrap_deferred
14
+ Focused:
15
+ - fcontext
16
+ - fdescribe
17
+ - fwrap_context
18
+ - fwrap_deferred
19
+ Helpers:
20
+ - let
21
+ - let!
22
+ - let?
23
+ Includes:
24
+ Examples:
25
+ - finclude_contract
26
+ - finclude_deferred
27
+ - finclude_examples
28
+ - fwrap_examples
29
+ - include_contract
30
+ - include_deferred
31
+ - include_examples
32
+ - wrap_examples
33
+ - xinclude_contract
34
+ - xinclude_deferred
35
+ - xinclude_examples
36
+ - xwrap_examples
37
+ SharedGroups:
38
+ Examples:
39
+ - deferred_examples
40
+ Context:
41
+ - deferred_context
@@ -1,107 +1,140 @@
1
- # lib/rspec/sleeping_king_studios/concerns/example_constants.rb
1
+ # frozen_string_literal: true
2
2
 
3
3
  require 'rspec/sleeping_king_studios/concerns'
4
4
 
5
5
  module RSpec::SleepingKingStudios::Concerns
6
- module ExampleConstants
6
+ # Methods for defining example-scoped classes and constants.
7
+ module ExampleConstants # rubocop:disable Metrics/ModuleLength
7
8
  DEFAULT_VALUE = Object.new.freeze
8
9
  private_constant :DEFAULT_VALUE
9
10
 
10
- def self.assign_constant namespace, constant_name, constant_value
11
- prior_value = DEFAULT_VALUE
11
+ class << self
12
+ # @api private
13
+ def define_class(class_name:, example:, base_class: nil, &block)
14
+ klass = Class.new(resolve_base_class(base_class))
12
15
 
13
- if namespace.const_defined?(constant_name)
14
- prior_value = namespace.const_get(constant_name)
15
- end # if
16
+ klass.define_singleton_method(:name) { class_name }
17
+ klass.singleton_class.send(:alias_method, :inspect, :name)
18
+ klass.singleton_class.send(:alias_method, :to_s, :name)
16
19
 
17
- namespace.const_set(constant_name, constant_value)
20
+ example.instance_exec(klass, &block) if block_given?
18
21
 
19
- yield
20
- ensure
21
- if prior_value == DEFAULT_VALUE
22
- namespace.send :remove_const, constant_name
23
- else
24
- namespace.const_set(constant_name, prior_value)
25
- end # if-else
26
- end # class method assign_constant
22
+ klass
23
+ end
27
24
 
28
- def self.guard_existing_constant! namespace, constant_name
29
- return unless namespace.const_defined?(constant_name)
25
+ # @api private
26
+ def with_constant( # rubocop:disable Metrics/MethodLength
27
+ constant_name:,
28
+ constant_value:,
29
+ namespace:,
30
+ force: false
31
+ )
32
+ guard_existing_constant!(namespace, constant_name) unless force
30
33
 
31
- message =
32
- "constant #{constant_name} is already defined with value "\
33
- "#{namespace.const_get(constant_name).inspect}"
34
+ prior_value = DEFAULT_VALUE
34
35
 
35
- raise NameError, message
36
- end # class method guard_existing_constant!
36
+ if namespace.const_defined?(constant_name)
37
+ prior_value = namespace.const_get(constant_name)
38
+ end
37
39
 
38
- def self.resolve_base_class value
39
- value = value.fetch(:base_class, nil) if value.is_a?(Hash)
40
+ namespace.const_set(constant_name, constant_value)
40
41
 
41
- return Object if value.nil?
42
+ yield
43
+ ensure
44
+ if prior_value == DEFAULT_VALUE
45
+ namespace.send :remove_const, constant_name
46
+ else
47
+ namespace.const_set(constant_name, prior_value)
48
+ end
49
+ end
42
50
 
43
- return Object.const_get(value) if value.is_a?(String)
51
+ # @api private
52
+ def with_namespace(module_names) # rubocop:disable Metrics/MethodLength
53
+ last_defined = nil
44
54
 
45
- value
46
- end
55
+ resolved =
56
+ module_names.reduce(Object) do |namespace, module_name|
57
+ if namespace.const_defined?(module_name)
58
+ next namespace.const_get(module_name)
59
+ end
47
60
 
48
- def self.resolve_namespace module_names
49
- last_defined = nil
61
+ last_defined ||= { namespace:, module_name: }
50
62
 
51
- resolved =
52
- module_names.reduce(Object) do |ns, module_name|
53
- next ns.const_get(module_name) if ns.const_defined?(module_name)
63
+ namespace.const_set(module_name, Module.new)
64
+ end
54
65
 
55
- last_defined ||= { :namespace => ns, :module_name => module_name }
66
+ yield resolved
67
+ ensure
68
+ if last_defined
69
+ last_defined[:namespace]
70
+ .send(:remove_const, last_defined[:module_name])
71
+ end
72
+ end
56
73
 
57
- ns.const_set(module_name, Module.new)
58
- end # reduce
74
+ private
59
75
 
60
- yield resolved
61
- ensure
62
- if last_defined
63
- last_defined[:namespace].send(:remove_const, last_defined[:module_name])
64
- end # if
65
- end # class method resolve_namespace
76
+ def guard_existing_constant!(namespace, constant_name)
77
+ return unless namespace.const_defined?(constant_name)
66
78
 
67
- def example_class class_name, base_class = nil, &block
68
- class_name = class_name.to_s if class_name.is_a?(Symbol)
79
+ message =
80
+ "constant #{constant_name} is already defined with value " \
81
+ "#{namespace.const_get(constant_name).inspect}"
69
82
 
70
- example_constant(class_name) do
71
- klass = Class.new(ExampleConstants.resolve_base_class(base_class))
83
+ raise NameError, message
84
+ end
72
85
 
73
- klass.define_singleton_method(:name) { class_name }
74
- klass.singleton_class.send(:alias_method, :inspect, :name)
75
- klass.singleton_class.send(:alias_method, :to_s, :name)
86
+ def resolve_base_class(value)
87
+ value = value.fetch(:base_class, nil) if value.is_a?(Hash)
76
88
 
77
- instance_exec(klass, &block) if block_given?
89
+ return Object if value.nil?
78
90
 
79
- klass
80
- end # example_constant
81
- end # method example_class
91
+ return Object.const_get(value) if value.is_a?(String)
82
92
 
83
- def example_constant qualified_name, constant_value = DEFAULT_VALUE, force: false, &block
84
- around(:example) do |wrapped_example|
85
- example = wrapped_example.example
93
+ value
94
+ end
95
+ end
96
+
97
+ def example_class(class_name, base_class = nil, &block)
98
+ class_name = class_name.to_s if class_name.is_a?(Symbol)
99
+
100
+ example_constant(class_name) do
101
+ ExampleConstants.define_class(
102
+ base_class:,
103
+ class_name:,
104
+ example: self,
105
+ &block
106
+ )
107
+ end
108
+ end
86
109
 
110
+ def example_constant( # rubocop:disable Metrics/MethodLength
111
+ qualified_name,
112
+ constant_value = DEFAULT_VALUE,
113
+ force: false,
114
+ &block
115
+ )
116
+ around(:example) do |wrapped_example|
87
117
  resolved_value =
88
- if constant_value == DEFAULT_VALUE
89
- block ? example.instance_exec(&block) : nil
118
+ if constant_value == DEFAULT_VALUE && block_given?
119
+ wrapped_example.example.instance_exec(&block)
90
120
  else
91
121
  constant_value
92
- end # if
93
-
94
- module_names = qualified_name.to_s.split('::')
95
- constant_name = module_names.pop
96
-
97
- ExampleConstants.resolve_namespace(module_names) do |namespace|
98
- ExampleConstants.guard_existing_constant!(namespace, constant_name) unless force
99
-
100
- ExampleConstants.assign_constant(namespace, constant_name, resolved_value) do
122
+ end
123
+
124
+ *module_names, constant_name = qualified_name.to_s.split('::')
125
+
126
+ ExampleConstants.with_namespace(module_names) do |namespace|
127
+ ExampleConstants.with_constant(
128
+ constant_name:,
129
+ constant_value: resolved_value,
130
+ namespace:,
131
+ force:
132
+ ) \
133
+ do
101
134
  wrapped_example.call
102
- end # assign_constant
103
- end # resolve_namespace
104
- end # before example
105
- end # method example_constant
106
- end # module
107
- end # module
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec/sleeping_king_studios/concerns'
4
+
5
+ module RSpec::SleepingKingStudios::Concerns
6
+ # Methods for defining memoized helpers in example groups.
7
+ module MemoizedHelpers
8
+ # Defines a memoized helper if a method of the same name is not defined.
9
+ #
10
+ # @param method_name [String] the name of the helper method.
11
+ #
12
+ # @yieldreturn [Object] the value of the memoized helper method.
13
+ def let?(method_name, &)
14
+ return method_name if method_defined?(method_name)
15
+
16
+ let(method_name, &)
17
+ end
18
+ end
19
+ end
@@ -69,8 +69,11 @@ module RSpec::SleepingKingStudios::Concerns
69
69
  # example group by including the module. The shared examples must be
70
70
  # defined before including the module, or they will not be available in the
71
71
  # example group.
72
- def shared_examples name, *metadata_args, &block
73
- RSpec.world.shared_example_group_registry.add(self, name, *metadata_args, &block)
72
+ def shared_examples(name, *metadata_args, **metadata_kwargs, &block)
73
+ RSpec
74
+ .world
75
+ .shared_example_group_registry
76
+ .add(self, name, *metadata_args, **metadata_kwargs, &block)
74
77
  end # method shared_examples
75
78
  alias_method :shared_context, :shared_examples
76
79
 
@@ -1,8 +1,13 @@
1
- # lib/rspec/sleeping_king_studios/concerns.rb
1
+ # frozen_string_literal: true
2
2
 
3
3
  require 'rspec/sleeping_king_studios'
4
4
 
5
5
  module RSpec::SleepingKingStudios
6
6
  # RSpec-related concerns to mix into classes or objects.
7
- module Concerns; end
8
- end # module
7
+ module Concerns
8
+ autoload :ExampleConstants,
9
+ 'rspec/sleeping_king_studios/concerns/example_constants'
10
+ autoload :MemoizedHelpers,
11
+ 'rspec/sleeping_king_studios/concerns/memoized_helpers'
12
+ end
13
+ end