rspec_magic 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
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.