rspec_magic 0.1.2

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 (38) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +26 -0
  3. data/.rspec +3 -0
  4. data/.yardopts +7 -0
  5. data/Gemfile +8 -0
  6. data/Gemfile.lock +36 -0
  7. data/MIT-LICENSE +20 -0
  8. data/README-ru.md +328 -0
  9. data/README.md +328 -0
  10. data/Rakefile +15 -0
  11. data/lib/rspec_magic/config.rb +17 -0
  12. data/lib/rspec_magic/stable/alias_method.rb +26 -0
  13. data/lib/rspec_magic/stable/context_when.rb +101 -0
  14. data/lib/rspec_magic/stable/described_sym.rb +62 -0
  15. data/lib/rspec_magic/stable/use_letset.rb +95 -0
  16. data/lib/rspec_magic/stable/use_method_discovery.rb +75 -0
  17. data/lib/rspec_magic/stable.rb +13 -0
  18. data/lib/rspec_magic/unstable/include_dir_context.rb +39 -0
  19. data/lib/rspec_magic/unstable.rb +13 -0
  20. data/lib/rspec_magic/version.rb +5 -0
  21. data/lib/rspec_magic.rb +11 -0
  22. data/rspec_magic.gemspec +17 -0
  23. data/spec/lib/rspec_magic/alias_method_spec.rb +19 -0
  24. data/spec/lib/rspec_magic/context_when_spec.rb +45 -0
  25. data/spec/lib/rspec_magic/described_sym_spec.rb +39 -0
  26. data/spec/lib/rspec_magic/include_dir_context/README.md +2 -0
  27. data/spec/lib/rspec_magic/include_dir_context/_context.rb +4 -0
  28. data/spec/lib/rspec_magic/include_dir_context/app/_context.rb +4 -0
  29. data/spec/lib/rspec_magic/include_dir_context/app/idc_consumer_spec.rb +15 -0
  30. data/spec/lib/rspec_magic/include_dir_context/app/models/_context.rb +4 -0
  31. data/spec/lib/rspec_magic/include_dir_context/app/models/idc_consumer_spec.rb +15 -0
  32. data/spec/lib/rspec_magic/include_dir_context/idc_consumer_spec.rb +15 -0
  33. data/spec/lib/rspec_magic/use_letset_spec.rb +134 -0
  34. data/spec/lib/rspec_magic/use_method_discovery_spec.rb +24 -0
  35. data/spec/spec_helper.rb +9 -0
  36. data/spec/support/self.rb +6 -0
  37. data/spec/support/simplecov.rb +11 -0
  38. metadata +80 -0
data/README.md ADDED
@@ -0,0 +1,328 @@
1
+
2
+ # A little bit of magic for RSpec tests
3
+
4
+ <!-- @import "[TOC]" {cmd="toc" depthFrom=2 depthTo=6 orderedList=false} -->
5
+
6
+ <!-- code_chunk_output -->
7
+
8
+ - [Overview](#overview)
9
+ - [Setup](#setup)
10
+ - [Features](#features)
11
+ - [`alias_method`](#alias_method)
12
+ - [`context_when`](#context_when)
13
+ - [`described_sym`](#described_sym)
14
+ - [`include_dir_context`](#include_dir_context)
15
+ - [`use_letset`](#use_letset)
16
+ - [`use_method_discovery`](#use_method_discovery)
17
+ - [Details](#details)
18
+ - [On setup](#on-setup)
19
+ - [On `context_when`](#on-context_when)
20
+ - [On `include_dir_context`](#on-include_dir_context)
21
+ - [Copyright](#copyright)
22
+
23
+ <!-- /code_chunk_output -->
24
+
25
+ ## Overview
26
+
27
+ 🆎 *Этот текст можно прочитать на русском языке: [README-ru.md](README-ru.md).*
28
+
29
+ RSpecMagic is a set of extensions for writing compact and expressive tests.
30
+
31
+ ## Setup
32
+
33
+ > 💡 *It's assumed that you've already set up RSpec in your project.*
34
+
35
+ Add to your project's `Gemfile`:
36
+
37
+ ```ruby
38
+ gem "rspec_magic"
39
+ #gem "rspec_magic", git: "https://github.com/dadooda/rspec_magic"
40
+ ```
41
+
42
+ Add to your RSpec startup file (usually `spec/spec_helper.rb`):
43
+
44
+ ```ruby
45
+ require "rspec_magic/stable"
46
+ require "rspec_magic/unstable"
47
+
48
+ RSpecMagic::Config.spec_path = File.expand_path(".", __dir__)
49
+ ```
50
+
51
+ The `spec_path` is used by some of the features, notably, [include_dir_context](#include_dir_context).
52
+ The computed path should point to `spec/` of the project's directory.
53
+
54
+ See [Details](#on-setup).
55
+
56
+ ## Features
57
+
58
+ ### `alias_method`
59
+
60
+ A matcher to check that a method is an alias of another method.
61
+
62
+ ```ruby
63
+ describe User do
64
+ it { is_expected.to alias_method(:admin?, :is_admin) }
65
+ end
66
+ ```
67
+
68
+ ### `context_when`
69
+
70
+ Create a self-descriptive `"when …"` context with one or more `let` variables defined.
71
+ The blocks below are synonymous.
72
+
73
+ ```ruby
74
+ context_when name: "Joe", age: 25 do
75
+ it do
76
+ expect([name, age]).to eq ["Joe", 25]
77
+ end
78
+ end
79
+ ```
80
+
81
+ ```ruby
82
+ context "when { name: \"Joe\", age: 25 }" do
83
+ let(:name) { "Joe" }
84
+ let(:age) { 25 }
85
+ it do
86
+ expect([name, age]).to eq ["Joe", 25]
87
+ end
88
+ end
89
+ ```
90
+
91
+ See [Details](#on-context_when).
92
+
93
+ ### `described_sym`
94
+
95
+ Transform `described_class` into underscored symbols `described_sym` and `me`.
96
+
97
+ ```ruby
98
+ describe UserProfile do
99
+ it { expect(described_sym).to eq :user_profile }
100
+ it { expect(me).to eq :user_profile }
101
+ end
102
+ ```
103
+
104
+ Common usage with a factory:
105
+
106
+ ```ruby
107
+ describe UserProfile do
108
+ let(:uprof1) { create described_sym }
109
+ let(:uprof2) { create me }
110
+
111
+ end
112
+ ```
113
+
114
+ ### `include_dir_context`
115
+
116
+ ♒︎ *This feature was added recently and may change.*
117
+
118
+ Organize shared contexts ([shared_context](https://rspec.info/features/3-12/rspec-core/example-groups/shared-context/)) in a hierarchy.
119
+ Import relevant shared contexts into the given test.
120
+
121
+ Follow these steps:
122
+
123
+ 1. Make sure that `RSpecMagic::Config.spec_path` is configured correctly.
124
+ It must point to project's `spec/`.
125
+
126
+ 2. Across the spec directory tree, create the shared context files, each named `_context.rb`.
127
+ A typical `_context.rb` looks like this:
128
+
129
+ ```ruby
130
+ shared_context __dir__ do
131
+
132
+ end
133
+ ```
134
+
135
+ 3. Add to your hypothetical `spec_helper.rb`:
136
+
137
+ ```ruby
138
+ # Load the shared contexts hierarchy.
139
+ Dir[File.expand_path("**/_context.rb", __dir__)].each { |fn| require fn }
140
+ ```
141
+
142
+ 4. In the given spec file, add a call to `include_dir_context` in the body of the outermost `describe`:
143
+
144
+ ```ruby
145
+ describe … do
146
+ include_dir_context __dir__
147
+
148
+ end
149
+ ```
150
+
151
+ Say, our spec file is `spec/app/controllers/api/player_controller_spec.rb`.
152
+
153
+ The main `describe` will load the contexts from the following files, if any:
154
+
155
+ ```
156
+ spec/_context.rb
157
+ spec/app/_context.rb
158
+ spec/app/controllers/_context.rb
159
+ spec/app/controllers/api/_context.rb
160
+ ```
161
+
162
+ See [Details](#on-include_dir_context).
163
+
164
+ ### `use_letset`
165
+
166
+ Define a method to create `let` variables, which comprise a `Hash` collection.
167
+
168
+ ```ruby
169
+ describe do
170
+ # Method is `let_a`. Collection is `attrs`.
171
+ use_letset :let_a, :attrs
172
+
173
+ # Declare `attrs` elements.
174
+ let_a(:age)
175
+ let_a(:name)
176
+
177
+ subject { attrs }
178
+
179
+ # None of the elements is set yet.
180
+ it { is_expected.to eq({}) }
181
+
182
+ # Set `name` and see it in the collection.
183
+ context_when name: "Joe" do
184
+ it { is_expected.to eq(name: "Joe") }
185
+
186
+ # Add `age` and see both in the collection.
187
+ context_when age: 25 do
188
+ it { is_expected.to eq(name: "Joe", age: 25) }
189
+ end
190
+ end
191
+ end
192
+ ```
193
+
194
+ When used with a block, `let_a` behaves like a regular `let`:
195
+
196
+ ```ruby
197
+ describe do
198
+ use_letset :let_a, :attrs
199
+
200
+ let_a(:age) { 25 }
201
+ let_a(:name) { "Joe" }
202
+
203
+ it { expect(attrs).to eq(name: "Joe", age: 25) }
204
+ end
205
+ ```
206
+
207
+ ### `use_method_discovery`
208
+
209
+ Create an automatic `let` variable containing the method or action name,
210
+ computed from the description of the parent `describe`.
211
+
212
+ ```ruby
213
+ describe do
214
+ use_method_discovery :m
215
+
216
+ subject { m }
217
+
218
+ describe "#first_name" do
219
+ it { is_expected.to eq :first_name }
220
+ end
221
+
222
+ describe ".some_stuff" do
223
+ it { is_expected.to eq :some_stuff }
224
+ end
225
+
226
+ describe "GET some_action" do
227
+ describe "intermediate context" do
228
+ it { is_expected.to eq :some_action } # (1)
229
+ end
230
+ end
231
+ end
232
+ ```
233
+
234
+ `m` finds the nearest *usable* context, whose description format allows
235
+ the extraction of the method name.
236
+ At the line marked (1) above, `m` ignores the loosely formatted `"intermediate context"`
237
+ and grabs the data from `"GET some_action"`.
238
+
239
+ ## Details
240
+
241
+ ### On setup
242
+
243
+ 1. `stable` and `unstable` are feature sets.
244
+ Set `unstable` contains recently added features that may change in the coming versions.
245
+
246
+ 2. It's possible to include specific features only. Example:
247
+
248
+ ```ruby
249
+ require "rspec_magic/stable/use_method_discovery"
250
+ ```
251
+
252
+ ### On `context_when`
253
+
254
+ 1. The context can be temporarily excluded by prepending `x`:
255
+
256
+ ```ruby
257
+ xcontext_when … do
258
+
259
+ end
260
+ ```
261
+
262
+ 2. It's possible to define a custom report line formatter:
263
+
264
+ ```ruby
265
+ describe "…" do
266
+ def self._context_when_formatter(h)
267
+ "when #{h.to_json}"
268
+ end
269
+
270
+ context_when … do
271
+
272
+ end
273
+ end
274
+ ```
275
+
276
+ 3. `context_when` works nicely with [use_letset](#use_letset),
277
+ usually to set the attributes of the object being tested.
278
+
279
+ 4. The values of `let` belong to the `describe` level.
280
+ If you need values computed at the `it` level, use the traditional `let(…) { … }`
281
+ inside the context.
282
+
283
+ ### On `include_dir_context`
284
+
285
+ There's a cool thing in RSpec — [shared_context](https://rspec.info/features/3-12/rspec-core/example-groups/shared-context/).
286
+ The idea is simple: somewhere (in a hypothetical `spec_helper.rb`) fold something reusable in a `shared_context "this and that"`,
287
+ and then import that stuff via `include_context "this and that"` where we need it.
288
+
289
+ We can put anything in `shared_context` — reusable tests, `let` variables, *but most importantly* —
290
+ methods, both belonging to the `describe` level (`def self.doit`) and the `it` level (`def doit`).
291
+
292
+ Sounds like a library, innit?
293
+
294
+ There's a pinch of salt though.
295
+ Existing means of contexts management are very primitive and rely solely on unique global names.
296
+
297
+ RSpec doesn't let us organize shared contexts in a hierarchy,
298
+ and import them automatically into groups of spec files, such as:
299
+ *all* model tests get context M, *all* controller tests get context C, and *all* at once get context A.
300
+
301
+ In order to maintain minimal order, one has to come up with unique names for shared contexts,
302
+ and list those contexts *in each and every* spec file:
303
+
304
+ ```ruby
305
+ describe … do
306
+ include_context "basic"
307
+ include_context "controllers"
308
+ include_context "api_controllers"
309
+
310
+ end
311
+ ```
312
+
313
+ If you see anything like this, give your kudos to the author, as this is an advanced level.
314
+ Most of the time, people don't even do that, but simply dump all of their extensions
315
+ into some “helper”-like pile of cr%p, justifying it by stating that “there was no time to sort it out”.
316
+
317
+ What does `include_dir_context` have to offer?
318
+
319
+ 1. The means to organize shared contexts in a hierarchy.
320
+ 2. The means to automatically import sets of *what is needed* into *where it's needed.*
321
+
322
+ See the [main chapter](#include_dir_context) for a step-by-step example.
323
+
324
+ ## Copyright
325
+
326
+ The product is free software distributed under the terms of the MIT license.
327
+
328
+ — © 2017-2024 Alex Fortuna
data/Rakefile ADDED
@@ -0,0 +1,15 @@
1
+
2
+ desc "Build the gem"
3
+ task :build do
4
+ system "gem build rspec_magic.gemspec"
5
+ end
6
+
7
+ desc "Build YARD docs"
8
+ task :doc do
9
+ system "bundle exec yard"
10
+ end
11
+
12
+ desc "Run tests"
13
+ task :test do
14
+ system "bundle exec rspec"
15
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecMagic
4
+ # The configuration.
5
+ module Config
6
+ class << self
7
+ attr_writer :spec_path
8
+
9
+ # The path of where the specs are.
10
+ # Most commonly, <tt>spec/</tt> of the project's directory.
11
+ # @return [String]
12
+ def spec_path
13
+ @spec_path || raise("`#{__method__}` must be configured")
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecMagic; module Stable
4
+ # A matcher to check that a method is an alias of another method.
5
+ #
6
+ # describe User do
7
+ # it { is_expected.to alias_method(:admin?, :is_admin) }
8
+ # end
9
+ #
10
+ module AliasMethod
11
+ end
12
+
13
+ # NOTE: `RSpec` has an autoloader of its own. Constants might not respond until we touch them.
14
+ begin; RSpec::Matchers; rescue NameError; end
15
+
16
+ # Activate.
17
+ defined?(RSpec::Matchers) && RSpec::Matchers.respond_to?(:define) and RSpec::Matchers.define(:alias_method) do |new_name, old_name|
18
+ match do |subject|
19
+ expect(subject.method(new_name)).to eq subject.method(old_name)
20
+ end
21
+
22
+ description do
23
+ "have #{new_name.inspect} aliased to #{old_name.inspect}"
24
+ end
25
+ end
26
+ end; end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ "LODoc"
4
+
5
+ module RSpecMagic; module Stable
6
+ # Create a self-descriptive <tt>"when …"</tt> context with one or more +let+ variables defined.
7
+ # The blocks below are synonymous.
8
+ #
9
+ # context_when name: "Joe", age: 25 do
10
+ # it do
11
+ # expect([name, age]).to eq ["Joe", 25]
12
+ # end
13
+ # end
14
+ #
15
+ # context "when { name: \"Joe\", age: 25 }" do
16
+ # let(:name) { "Joe" }
17
+ # let(:age) { 25 }
18
+ # it do
19
+ # expect([name, age]).to eq ["Joe", 25]
20
+ # end
21
+ # end
22
+ #
23
+ # = Features
24
+ #
25
+ # Prepend +x+ to +context_when+ to exclude it:
26
+ #
27
+ # xcontext_when … do
28
+ # …
29
+ # end
30
+ #
31
+ # ---
32
+ #
33
+ # Define a custom report line formatter:
34
+ #
35
+ # describe "…" do
36
+ # def self._context_when_formatter(h)
37
+ # "when #{h.to_json}"
38
+ # end
39
+ #
40
+ # context_when … do
41
+ # …
42
+ # end
43
+ # end
44
+ #
45
+ module ContextWhen
46
+ module Exports
47
+ # Default formatter for {#context_when}. Defined your custom one at the +describe+ level if needed.
48
+ # @param [Hash] h
49
+ # @return [String]
50
+ def _context_when_formatter(h)
51
+ # Extract labels for Proc arguments, if any.
52
+ labels = {}
53
+ h.each do |k, v|
54
+ if v.is_a? Proc
55
+ begin
56
+ labels[k] = h.fetch(lk = "#{k}_label".to_sym)
57
+ h.delete(lk)
58
+ rescue KeyError
59
+ raise ArgumentError, "`#{k}` is a `Proc`, `#{k}_label` must be given"
60
+ end
61
+ end
62
+ end
63
+
64
+ pcs = h.map do |k, v|
65
+ [
66
+ k.is_a?(Symbol) ? "#{k}:" : "#{k.inspect} =>",
67
+ v.is_a?(Proc) ? labels[k] : v.inspect,
68
+ ].join(" ")
69
+ end
70
+
71
+ "when { " + pcs.join(", ") + " }"
72
+ end
73
+
74
+ # Create a context.
75
+ # @param [Hash] h
76
+ def context_when(h, &block)
77
+ context _context_when_formatter(h) do
78
+ h.each do |k, v|
79
+ if v.is_a? Proc
80
+ let(k, &v)
81
+ else
82
+ # Generic scalar value.
83
+ let(k) { v }
84
+ end
85
+ end
86
+ class_eval(&block)
87
+ end
88
+ end
89
+
90
+ # Create a temporarily excluded context.
91
+ def xcontext_when(h, &block)
92
+ xcontext _context_when_formatter(h) { class_eval(&block) }
93
+ end
94
+ end # Exports
95
+ end # module
96
+
97
+ # Activate.
98
+ defined?(RSpec) && RSpec.respond_to?(:configure) and RSpec.configure do |config|
99
+ config.extend ContextWhen::Exports
100
+ end
101
+ end; end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ "LODoc"
4
+
5
+ module RSpecMagic; module Stable
6
+ # Transform +described_class+ into underscored symbols +described_sym+ and +me+.
7
+ #
8
+ # describe UserProfile do
9
+ # it { expect(described_sym).to eq :user_profile }
10
+ # it { expect(me).to eq :user_profile }
11
+ # end
12
+ #
13
+ # With a factory:
14
+ #
15
+ # describe UserProfile do
16
+ # let(:uprof1) { create described_sym }
17
+ # let(:uprof2) { create me }
18
+ # …
19
+ # end
20
+ module DescribedSym
21
+ module Exports
22
+ # A +Symbol+ representation of +described_class+.
23
+ # @return [Symbol] E.g. +:user_profile+.
24
+ def described_sym
25
+ Util.underscore(described_class.to_s).to_sym
26
+ end
27
+ alias_method :me, :described_sym
28
+ end # Exports
29
+
30
+ # Utilities.
31
+ module Util
32
+ # Generate an underscored, lowercase representation of a camel-cased word.
33
+ # <tt>"::"</tt>s are converted to <tt>"/"</tt>s.
34
+ #
35
+ # underscore("ActiveModel") # => "active_model"
36
+ # underscore("ActiveModel::Errors") # => "active_model/errors"
37
+ #
38
+ # NOTE: This method has been ported from ActiveSupport, mostly as is.
39
+ #
40
+ # @param [String] camel_cased_word
41
+ # @return [Symbol]
42
+ def self.underscore(camel_cased_word)
43
+ # Unlike ActiveSupport, we don't support acronyms.
44
+ acronym_regex = /(?=a)b/
45
+
46
+ word = camel_cased_word.to_s.dup
47
+ word.gsub!(/::/, '/')
48
+ word.gsub!(/(?:([A-Za-z\d])|^)(#{acronym_regex})(?=\b|[^a-z])/) { "#{$1}#{$1 && '_'}#{$2.downcase}" }
49
+ word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2')
50
+ word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
51
+ word.tr!("-", "_")
52
+ word.downcase!
53
+ word
54
+ end
55
+ end
56
+ end # module
57
+
58
+ # Activate.
59
+ defined?(RSpec) && RSpec.respond_to?(:configure) and RSpec.configure do |config|
60
+ config.include DescribedSym::Exports
61
+ end
62
+ end; end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ "LODoc"
4
+
5
+ module RSpecMagic; module Stable
6
+ # Define a method to create +let+ variables, which comprise a +Hash+ collection.
7
+ #
8
+ # describe do
9
+ # # Method is `let_a`. Collection is `attrs`.
10
+ # use_letset :let_a, :attrs
11
+ #
12
+ # # Declare `attrs` elements.
13
+ # let_a(:age)
14
+ # let_a(:name)
15
+ #
16
+ # subject { attrs }
17
+ #
18
+ # # None of the elements is set yet.
19
+ # it { is_expected.to eq({}) }
20
+ #
21
+ # # Set `name` and see it in the collection.
22
+ # context_when name: "Joe" do
23
+ # it { is_expected.to eq(name: "Joe") }
24
+ #
25
+ # # Add `age` and see both in the collection.
26
+ # context_when age: 25 do
27
+ # it { is_expected.to eq(name: "Joe", age: 25) }
28
+ # end
29
+ # end
30
+ # end
31
+ #
32
+ # When used with a block, +let_a+ behaves like a regular +let+:
33
+ #
34
+ # describe do
35
+ # use_letset :let_a, :attrs
36
+ #
37
+ # let_a(:age) { 25 }
38
+ # let_a(:name) { "Joe" }
39
+ #
40
+ # it { expect(attrs).to eq(name: "Joe", age: 25) }
41
+ # end
42
+ #
43
+ module UseLetset
44
+ module Exports
45
+ # Define the collection.
46
+ # @param let_method [Symbol]
47
+ # @param collection_let [Symbol]
48
+ def use_letset(let_method, collection_let)
49
+ keys_m = "_#{collection_let}_keys".to_sym
50
+
51
+ # See "Implementation notes" on failed implementation of "collection only" mode.
52
+
53
+ # E.g. "_data_keys" or something.
54
+ define_singleton_method(keys_m) do
55
+ if instance_variable_defined?(k = "@#{keys_m}")
56
+ instance_variable_get(k)
57
+ else
58
+ # Start by copying superclass's known vars or default to `[]`.
59
+ instance_variable_set(k, (superclass.send(keys_m).dup rescue []))
60
+ end
61
+ end
62
+
63
+ define_singleton_method let_method, ->(k, &block) do
64
+ (send(keys_m) << k).uniq!
65
+ # Create a `let` variable unless it's a declaration call (`let_a(:name)`).
66
+ let(k, &block) if block
67
+ end
68
+
69
+ define_method(collection_let) do
70
+ {}.tap do |h|
71
+ self.class.send(keys_m).each do |k|
72
+ h[k] = public_send(k) if respond_to?(k)
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end # Exports
78
+ end # module
79
+
80
+ # Activate.
81
+ defined?(RSpec) && RSpec.respond_to?(:configure) and RSpec.configure do |config|
82
+ config.extend UseLetset::Exports
83
+ end
84
+ end; end
85
+
86
+ #
87
+ # Implementation notes:
88
+ #
89
+ # * There was once an idea to support `use_letset` in "collection only" mode. Say, `let_a` appends
90
+ # to `attrs`, but doesn't publish a let variable. This change IS COMPLETELY NOT IN LINE with
91
+ # RSpec design. Let variables are methods and the collection is built by probing for those
92
+ # methods. "Collection only" would require a complete redesign. It's easier to implement another
93
+ # helper method for that, or, even better, do it with straight Ruby right in the test where
94
+ # needed. The need for "collection only" mode is incredibly rare, say, specific serializer
95
+ # tests.