let_each 0.1.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.
- checksums.yaml +7 -0
- data/LICENSE.txt +20 -0
- data/README.md +98 -0
- data/lib/let_each/extension.rb +116 -0
- data/lib/let_each/version.rb +3 -0
- data/lib/let_each.rb +8 -0
- metadata +98 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 649eafd9476ae0aeffe70f65df3351c3d2c4744f1a844039966cec1d155ac26a
|
|
4
|
+
data.tar.gz: 5a8cc3329f8b40133f36baaa2463a790938cbcfd8e1945c05a8f4a8848aa96bc
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: '0184e7882ccb883e2e3e79e6661a9e3dced08549ebcb72c50de9f0fb4f8d3e63ebb194be376ae5d21150c459455dd39f43906288ea3e50917c8ad8c6bcfb0994'
|
|
7
|
+
data.tar.gz: bb6ed3372685424c08f478baf9d94d7aeab508a0af694507576ee7c897092674b824892d4dd79b69a1706e828ea942c8ea6c303e2e91c54c39ac5edb33c90bfb
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) [2026] [Andrew Logsdon]
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
|
19
|
+
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+

|
|
2
|
+
# LetEach
|
|
3
|
+
(rspec-let-each)
|
|
4
|
+
|
|
5
|
+
`let_each` is an ergonomic RSpec helper that spawns context blocks with corresponding `let`s for each value in an array
|
|
6
|
+
|
|
7
|
+
a common dilemma I found when writing specs was
|
|
8
|
+
enumerating all edge cases is DRY and gives good coverage
|
|
9
|
+
but it requires too much block nesting/boiler plate.
|
|
10
|
+
Also, contexts written like this are not compatible with `let`s,
|
|
11
|
+
without some hackery.
|
|
12
|
+
|
|
13
|
+
e.g.
|
|
14
|
+
```ruby
|
|
15
|
+
[foo,bar].each do |x_local|
|
|
16
|
+
context "with x=#{x_local}" do
|
|
17
|
+
let(:x) { x_local }
|
|
18
|
+
|
|
19
|
+
it_behaves_like 'an example'
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
alternatively, let + sample is succint
|
|
25
|
+
but it gives flakey coverage
|
|
26
|
+
e.g.
|
|
27
|
+
```ruby
|
|
28
|
+
let(:x) { [foo, bar].sample }
|
|
29
|
+
|
|
30
|
+
it_behaves_like 'an example'
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
with this helper, we should be able to have the best of both worlds
|
|
34
|
+
- same coverage as first example
|
|
35
|
+
- compatible with `let` variables
|
|
36
|
+
- same succintness as second example
|
|
37
|
+
```ruby
|
|
38
|
+
let_each(:x, 2) { [foo, bar] }
|
|
39
|
+
|
|
40
|
+
it_behaves_like 'an example'
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Using the `let_each` by itself can lead to some awkward patterns when zipping expectations into the examples.
|
|
44
|
+
That's why I added a chainable method `with`, intended for the corresponding expectations, or possibly actions.
|
|
45
|
+
(The length of all chained `with`s is assumed to be the same as the parent `let_each`)
|
|
46
|
+
now we might write
|
|
47
|
+
```ruby
|
|
48
|
+
subject { test_method(x) }
|
|
49
|
+
|
|
50
|
+
let_each(:x, 2) { [foo, bar] }
|
|
51
|
+
.with(:expected_x) { [foo_expect, bar_expect] }
|
|
52
|
+
|
|
53
|
+
it { is_expected.to eq(expected_x) }
|
|
54
|
+
```
|
|
55
|
+
it can be continually chained, in case we need variables in tripples or quads too, why not.
|
|
56
|
+
|
|
57
|
+
## Installation
|
|
58
|
+
|
|
59
|
+
Add this line to your application's `Gemfile` typically inside the `:test` group:
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
group :test do
|
|
63
|
+
gem 'let_each'
|
|
64
|
+
end
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Setup
|
|
68
|
+
|
|
69
|
+
`let_each` is configured to automatically mix into RSpec's example groups when loaded.
|
|
70
|
+
In some projects, the `lib` folder is automatically loaded. If not, just add a require for `let_each` in your test setup.
|
|
71
|
+
```ruby
|
|
72
|
+
# spec/spec_helper.rb
|
|
73
|
+
require 'let_each'
|
|
74
|
+
```
|
|
75
|
+
You can immediately start using let_each inside your describe or context blocks.
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
### Verifying Installation
|
|
79
|
+
You can verify that the helper is loaded correctly by running a simple test:
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
RSpec.describe "LetEach Integration" do
|
|
83
|
+
let_each(:val) { [1, 2] }
|
|
84
|
+
|
|
85
|
+
it "works" do
|
|
86
|
+
expect([1, 2]).to include(val)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Compatibility
|
|
92
|
+
|
|
93
|
+
`let_each` is tested against and supports:
|
|
94
|
+
- **Ruby:** 2.7, 3.0, 3.1, 3.2, 3.3, 3.4
|
|
95
|
+
- **RSpec:** 3.0 and newer
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
MIT
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
module LetEach
|
|
2
|
+
module Extension
|
|
3
|
+
# Usage:
|
|
4
|
+
# lazy signature
|
|
5
|
+
# let_each(:x, 2) { [let_foo, let_bar] }
|
|
6
|
+
# .with(:y) { [foo_expected, bar_expected] }
|
|
7
|
+
#
|
|
8
|
+
# eager signature
|
|
9
|
+
# let_each(:y, [eager_foo, eager_bar])
|
|
10
|
+
#
|
|
11
|
+
# the lazy_array_block plays nice with other `let`s, but contexts are eagerly evaluated
|
|
12
|
+
# so we need to provide a "length" to know how many contexts to spawn
|
|
13
|
+
# then the array values can still be lazily evaluated
|
|
14
|
+
# alternatively, just pass an eager array if you don't need the laziness on the values
|
|
15
|
+
#
|
|
16
|
+
# There is possibly some overhead to using this in its present state. It could be optimized more but this is just my POC for the feature.
|
|
17
|
+
# Careful not to exponentially spawn contexts, every call to this helper will multiply the number of examples
|
|
18
|
+
# In a future feature we may allow for automatically limiting the number of contexts, then trending towards `let(:x) { [foo, bar].sample }`
|
|
19
|
+
# AFAIK, this works in nested contexts, and with shared_examples/contexts,
|
|
20
|
+
# but I'd suggest pushing the usage as close to the actual examples as possible, so you don't enumerate too much
|
|
21
|
+
#
|
|
22
|
+
# I've added a chainable `with` method to allow for parallely assigned lets
|
|
23
|
+
# this can be also be chained multiple times
|
|
24
|
+
def let_each(name, length_or_array, &lazy_array_block)
|
|
25
|
+
if lazy_array_block
|
|
26
|
+
raise 'must specify the length when providing a lazy array block' unless length_or_array.is_a?(Integer)
|
|
27
|
+
length = length_or_array
|
|
28
|
+
else
|
|
29
|
+
raise 'must provide an array when not providing a lazy array block' unless length_or_array.respond_to?(:length)
|
|
30
|
+
array = length_or_array
|
|
31
|
+
length = array.length
|
|
32
|
+
lazy_array_block = -> { array }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
array_proc_key = :"_#{name}_array_proc"
|
|
36
|
+
# I just didn't handle this case. doing so is a bit tricky with the approach I used
|
|
37
|
+
# we'd need to replay the `it` overrides with the changed value removed
|
|
38
|
+
raise "let_each already used for key: #{name}" if instance_methods.include?(array_proc_key)
|
|
39
|
+
# `it` was already used in this context but we didn't get a chance to override it yet
|
|
40
|
+
if defined?(@context_leafs)
|
|
41
|
+
raise 'let_each used after an example. either nest in a new context or arrange `let_each` above examples'
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# behavior is also unexpected if we `let` with this same name, but I'm not going out of my way to guard against that
|
|
45
|
+
|
|
46
|
+
let(array_proc_key, &lazy_array_block) # memoize the array proc result
|
|
47
|
+
# `super` would only work the first time we call let_each per context. so we'll just closure it every time
|
|
48
|
+
old_it = method(:it).unbind
|
|
49
|
+
chainable = LetEachWithChainable.new(self, length)
|
|
50
|
+
define_singleton_method(:it) do |*args, &block|
|
|
51
|
+
if defined?(@context_leafs)
|
|
52
|
+
# we make a lot of contexts with this helper
|
|
53
|
+
# this is an improvement to reuse them when possible
|
|
54
|
+
# (when `it` is used multiple times in the same context)
|
|
55
|
+
# should be able to do something similar with `context` but I'm not worried about it right now
|
|
56
|
+
# new context = caches are dumped anyway, so there's probably not much to gain
|
|
57
|
+
@context_leafs.each do |leaf|
|
|
58
|
+
old_it.bind_call(leaf, *args, &block)
|
|
59
|
+
end
|
|
60
|
+
else
|
|
61
|
+
# first time we're calling `it` in this context
|
|
62
|
+
# instance variable will be inaccessible from within these context blocks
|
|
63
|
+
# so we assign the local variable too
|
|
64
|
+
@context_leafs = context_leafs = []
|
|
65
|
+
length.times do |i|
|
|
66
|
+
context "when #{name}[#{i}]" do
|
|
67
|
+
let(name) { send(array_proc_key)[i] }
|
|
68
|
+
chainable.each do |proc|
|
|
69
|
+
instance_exec(i, &proc)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
old_it.bind_call(self, *args, &block)
|
|
73
|
+
context_leafs << self
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
chainable
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
class LetEachWithChainable
|
|
82
|
+
attr_accessor :withs, :example_group
|
|
83
|
+
|
|
84
|
+
def initialize(example_group, length)
|
|
85
|
+
@example_group = example_group
|
|
86
|
+
@length = length
|
|
87
|
+
@withs = []
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def each(&block)
|
|
91
|
+
withs.each(&block)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def with(name, array = nil, &lazy_array_block)
|
|
95
|
+
if lazy_array_block
|
|
96
|
+
# length is assumed to be the same as the base let_each
|
|
97
|
+
raise 'dont need to provide a second argument when providing a lazy array block' if array
|
|
98
|
+
else
|
|
99
|
+
lazy_array_block = -> { array }
|
|
100
|
+
end
|
|
101
|
+
array_proc_key = :"_#{name}_array_proc"
|
|
102
|
+
# can memoize the proc right away
|
|
103
|
+
example_group.let(array_proc_key, &lazy_array_block)
|
|
104
|
+
# we can't unload the main `let` until we're in the context
|
|
105
|
+
# so just store the proc
|
|
106
|
+
withs << lambda do |index|
|
|
107
|
+
# self is the only variable not closured here
|
|
108
|
+
# we'll instance_exec this on the context
|
|
109
|
+
let(name) { send(array_proc_key)[index] }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
self
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
data/lib/let_each.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: let_each
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Andrew Logsdon
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rspec-core
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '3.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '3.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: bundler
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rake
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rspec
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
68
|
+
executables: []
|
|
69
|
+
extensions: []
|
|
70
|
+
extra_rdoc_files: []
|
|
71
|
+
files:
|
|
72
|
+
- LICENSE.txt
|
|
73
|
+
- README.md
|
|
74
|
+
- lib/let_each.rb
|
|
75
|
+
- lib/let_each/extension.rb
|
|
76
|
+
- lib/let_each/version.rb
|
|
77
|
+
homepage: https://github.com/Alogsdon/rspec-let-each
|
|
78
|
+
licenses:
|
|
79
|
+
- MIT
|
|
80
|
+
metadata: {}
|
|
81
|
+
rdoc_options: []
|
|
82
|
+
require_paths:
|
|
83
|
+
- lib
|
|
84
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '2.7'
|
|
89
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
90
|
+
requirements:
|
|
91
|
+
- - ">="
|
|
92
|
+
- !ruby/object:Gem::Version
|
|
93
|
+
version: '0'
|
|
94
|
+
requirements: []
|
|
95
|
+
rubygems_version: 4.0.4
|
|
96
|
+
specification_version: 4
|
|
97
|
+
summary: Ergonomic context spawning for RSpec
|
|
98
|
+
test_files: []
|