rspec_in_context 1.2.0 → 1.2.1
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 +4 -4
- data/.rubocop.yml +4 -0
- data/CHANGELOG.md +10 -1
- data/LLMS.md +388 -0
- data/README.md +211 -66
- data/examples/README.md +23 -0
- data/examples/contexts/active_job_context.rb +15 -0
- data/examples/contexts/authenticated_request_context.rb +27 -0
- data/examples/contexts/composed_context.rb +13 -0
- data/examples/contexts/frozen_time_context.rb +8 -0
- data/examples/contexts/inline_mailer_context.rb +15 -0
- data/examples/contexts/interactor_contract_context.rb +23 -0
- data/examples/usage/composed_context_spec.rb +15 -0
- data/examples/usage/interactor_spec.rb +23 -0
- data/examples/usage/nested_contexts_spec.rb +30 -0
- data/examples/usage/request_spec.rb +29 -0
- data/lib/rspec_in_context/context_management.rb +0 -2
- data/lib/rspec_in_context/in_context.rb +0 -2
- data/lib/rspec_in_context/version.rb +1 -3
- data/lib/rspec_in_context.rb +0 -2
- metadata +13 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e63cf4bd9f911ee7f0434682e2b104578555086eba35590ff0c8d72214ef2f48
|
|
4
|
+
data.tar.gz: d4737b49923e3a8080603987f4c490d19ed798f824ce0822dd0e349028683c79
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 589321b198f19691378d28b67aa92beb237ba1bf6ceca7f78fc6fee08ad983cf94467ce4b288007ae98e825ab57aaa441c872ee3a0234d674b88e7e733d2a762
|
|
7
|
+
data.tar.gz: fac8b3dd1d6bb726f51fdd3377fcdf10092129d0e005bbd3133ff7169bd6dcf4a22fdedb97dc77602890b489014e2105e76b82c0a8480316d00250802a4b026f
|
data/.rubocop.yml
CHANGED
|
@@ -42,6 +42,10 @@ Style/GuardClause:
|
|
|
42
42
|
Style/OneClassPerFile:
|
|
43
43
|
Enabled: false
|
|
44
44
|
|
|
45
|
+
# Ruby >= 3.2 required, frozen string literal comment is unnecessary noise
|
|
46
|
+
Style/FrozenStringLiteralComment:
|
|
47
|
+
Enabled: false
|
|
48
|
+
|
|
45
49
|
### End Prettier
|
|
46
50
|
Naming/MethodParameterName:
|
|
47
51
|
Enabled: false
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.2.1] - 2026-03-05
|
|
10
|
+
### Added
|
|
11
|
+
- `LLMS.md` — comprehensive API reference for LLMs
|
|
12
|
+
- `examples/` directory with real-world usage patterns (contexts and specs)
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- Complete rewrite of `README.md` with better structure, real-world examples, and comparison with `shared_examples`
|
|
16
|
+
|
|
9
17
|
## [1.2.0] - 2026-01-27
|
|
10
18
|
### Breaking
|
|
11
19
|
- **BREAKING** Minimum Ruby version is now 3.2
|
|
@@ -53,7 +61,8 @@ This is a release in order to test all type of actions
|
|
|
53
61
|
- Changelog
|
|
54
62
|
- Support ruby 3.0
|
|
55
63
|
|
|
56
|
-
[Unreleased]: https://github.com/zaratan/rspec_in_context/compare/v1.2.
|
|
64
|
+
[Unreleased]: https://github.com/zaratan/rspec_in_context/compare/v1.2.1...HEAD
|
|
65
|
+
[1.2.1]: https://github.com/zaratan/rspec_in_context/compare/v1.2.0...v1.2.1
|
|
57
66
|
[1.2.0]: https://github.com/zaratan/rspec_in_context/compare/v1.1.0.3...v1.2.0
|
|
58
67
|
[1.1.0.3]: https://github.com/zaratan/rspec_in_context/compare/v1.1.0.2...v1.1.0.3
|
|
59
68
|
[1.1.0.2]: https://github.com/zaratan/rspec_in_context/compare/v1.1.0.1...v1.1.0.2
|
data/LLMS.md
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
# rspec_in_context — LLM Reference
|
|
2
|
+
|
|
3
|
+
## What is this gem?
|
|
4
|
+
|
|
5
|
+
`rspec_in_context` is a Ruby gem that provides `define_context` / `in_context` as a replacement for RSpec's `shared_examples` / `it_behaves_like`. The key advantage: contexts can accept blocks that get injected at a specific point via `execute_tests`, making them composable like methods. Contexts can also accept arguments, be namespaced, and nest within each other.
|
|
6
|
+
|
|
7
|
+
## Setup
|
|
8
|
+
|
|
9
|
+
**Gemfile:**
|
|
10
|
+
```ruby
|
|
11
|
+
gem 'rspec_in_context'
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
**spec_helper.rb (or rails_helper.rb):**
|
|
15
|
+
```ruby
|
|
16
|
+
require 'rspec_in_context'
|
|
17
|
+
|
|
18
|
+
RSpec.configure do |config|
|
|
19
|
+
config.include RspecInContext
|
|
20
|
+
end
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Requires Ruby >= 3.2.
|
|
24
|
+
|
|
25
|
+
## API Reference
|
|
26
|
+
|
|
27
|
+
### `RSpec.define_context(name, namespace: nil, ns: nil, silent: true, print_context: nil, &block)`
|
|
28
|
+
|
|
29
|
+
Define a global context (available in all spec files). Use outside any `describe`/`context` block.
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
RSpec.define_context :with_frozen_time do
|
|
33
|
+
before { freeze_time }
|
|
34
|
+
execute_tests
|
|
35
|
+
end
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### `define_context(name, namespace: nil, ns: nil, silent: true, print_context: nil, &block)`
|
|
39
|
+
|
|
40
|
+
Define a scoped context (available only within the enclosing `describe`/`context` block). Use inside a spec.
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
RSpec.describe MyService do
|
|
44
|
+
define_context :with_valid_params do
|
|
45
|
+
let(:params) { { name: "test" } }
|
|
46
|
+
execute_tests
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
in_context :with_valid_params do
|
|
50
|
+
it "works" do
|
|
51
|
+
expect(MyService.call(params)).to be_success
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**Parameters:**
|
|
58
|
+
- `name` (String, Symbol) — required. The context identifier.
|
|
59
|
+
- `namespace:` / `ns:` (String, Symbol) — optional namespace to avoid name collisions.
|
|
60
|
+
- `silent:` (Boolean, default: `true`) — when `true`, wraps in anonymous context. When `false`, wraps in a named context visible in `--format doc`.
|
|
61
|
+
- `print_context:` (Boolean) — inverse of `silent`. `print_context: true` is equivalent to `silent: false`.
|
|
62
|
+
- `&block` — required. The context body.
|
|
63
|
+
|
|
64
|
+
### `in_context(name, *args, namespace: nil, ns: nil, &block)`
|
|
65
|
+
|
|
66
|
+
Use a previously defined context. The optional block is injected where `execute_tests` appears in the context definition.
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
in_context :with_frozen_time do
|
|
70
|
+
it "uses frozen time" do
|
|
71
|
+
expect(Time.current).to eq(Time.current) # always true
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# With arguments (no block)
|
|
76
|
+
in_context :validates_field, :email
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Parameters:**
|
|
80
|
+
- `name` (String, Symbol) — the context to use.
|
|
81
|
+
- `*args` — positional arguments passed to the context block.
|
|
82
|
+
- `namespace:` / `ns:` — namespace to look in.
|
|
83
|
+
- `&block` — optional. Injected at `execute_tests`.
|
|
84
|
+
|
|
85
|
+
### `execute_tests` / `instantiate_context`
|
|
86
|
+
|
|
87
|
+
Placeholder inside a `define_context` block. Marks where the caller's block (from `in_context`) will be injected.
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
RSpec.define_context :setup_env do
|
|
91
|
+
let(:user) { create(:user) }
|
|
92
|
+
before { sign_in user }
|
|
93
|
+
|
|
94
|
+
execute_tests # <-- caller's block runs here
|
|
95
|
+
end
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
These are aliases. `instanciate_context` also works but is deprecated (typo).
|
|
99
|
+
|
|
100
|
+
## Key Concepts
|
|
101
|
+
|
|
102
|
+
### Scoped vs Global contexts
|
|
103
|
+
|
|
104
|
+
- **Scoped**: `define_context` inside a `describe`/`context` — automatically removed when the block ends.
|
|
105
|
+
- **Global**: `RSpec.define_context` outside any describe — available everywhere, never cleaned up.
|
|
106
|
+
|
|
107
|
+
### Block injection (execute_tests)
|
|
108
|
+
|
|
109
|
+
The core feature. When you call `in_context :foo do ... end`, the block is stored and injected where `execute_tests` appears in the `:foo` definition. If `execute_tests` is absent and you pass a block, the block is silently ignored.
|
|
110
|
+
|
|
111
|
+
### Arguments
|
|
112
|
+
|
|
113
|
+
Context blocks can accept arguments via block parameters:
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
RSpec.define_context :validates_field do |field_name|
|
|
117
|
+
context "when #{field_name} is nil" do
|
|
118
|
+
let(field_name) { nil }
|
|
119
|
+
it { is_expected.not_to be_valid }
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
in_context :validates_field, :email
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Namespacing
|
|
127
|
+
|
|
128
|
+
Prevents name collisions when different parts of your test suite define contexts with the same name:
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
RSpec.define_context :valid_params, ns: :users do ... end
|
|
132
|
+
RSpec.define_context :valid_params, ns: :posts do ... end
|
|
133
|
+
|
|
134
|
+
in_context :valid_params, ns: :users do ... end
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
If the same name exists in multiple namespaces and you don't specify `ns:`, `AmbiguousContextName` is raised.
|
|
138
|
+
|
|
139
|
+
### Silent mode
|
|
140
|
+
|
|
141
|
+
- `silent: true` (default): `in_context` wraps in an anonymous `context {}` block — invisible in `--format doc`.
|
|
142
|
+
- `silent: false` or `print_context: true`: wraps in `context("context_name") {}` — visible in `--format doc`.
|
|
143
|
+
|
|
144
|
+
### Thread-local block stack for nesting
|
|
145
|
+
|
|
146
|
+
Nested `in_context` calls work correctly. The gem uses `Thread.current[:test_block_stack]` (a stack) to track which block should be injected at each nesting level.
|
|
147
|
+
|
|
148
|
+
## Common Patterns
|
|
149
|
+
|
|
150
|
+
### Pattern: Setup context (before/let + execute_tests)
|
|
151
|
+
|
|
152
|
+
The most common pattern. Set up state, then let the caller provide assertions.
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
RSpec.define_context :as_admin do
|
|
156
|
+
let(:current_user) { create(:user, role: :admin) }
|
|
157
|
+
before { sign_in current_user }
|
|
158
|
+
execute_tests
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
in_context :as_admin do
|
|
162
|
+
it "can access admin panel" do
|
|
163
|
+
get admin_path
|
|
164
|
+
expect(response).to have_http_status(:ok)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Pattern: Parameterized context (arguments)
|
|
170
|
+
|
|
171
|
+
Pass data to customize the context behavior.
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
RSpec.define_context :contract_validation do |required_fields|
|
|
175
|
+
required_fields.each do |field|
|
|
176
|
+
context "when #{field} is missing" do
|
|
177
|
+
let(field) { nil }
|
|
178
|
+
it("fails") { expect(subject).to be_a_failure }
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
in_context :contract_validation, %i[name email phone]
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Pattern: Built-in tests + execute_tests
|
|
187
|
+
|
|
188
|
+
Include both built-in tests and a placeholder for injected tests.
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
RSpec.define_context :authenticated_request do
|
|
192
|
+
let(:user) { create(:user) }
|
|
193
|
+
before { sign_in user }
|
|
194
|
+
|
|
195
|
+
context "without authentication" do
|
|
196
|
+
before { sign_out user }
|
|
197
|
+
it("returns 401") { expect(response).to have_http_status(:unauthorized) }
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
execute_tests # caller's authenticated tests go here
|
|
201
|
+
end
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Pattern: Inline define + use
|
|
205
|
+
|
|
206
|
+
For contexts only needed in one spec file.
|
|
207
|
+
|
|
208
|
+
```ruby
|
|
209
|
+
RSpec.describe ReportGenerator do
|
|
210
|
+
define_context :with_sample_data do
|
|
211
|
+
let!(:records) { create_list(:record, 5) }
|
|
212
|
+
execute_tests
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
in_context :with_sample_data do
|
|
216
|
+
it "generates the report" do
|
|
217
|
+
expect(ReportGenerator.call.rows.count).to eq(5)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Pattern: Nested in_context in define_context
|
|
224
|
+
|
|
225
|
+
Compose higher-level contexts from smaller ones.
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
RSpec.define_context :full_integration do
|
|
229
|
+
in_context :with_frozen_time do
|
|
230
|
+
in_context :as_admin do
|
|
231
|
+
execute_tests
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Pattern: File organization (spec/contexts/*.rb)
|
|
238
|
+
|
|
239
|
+
```
|
|
240
|
+
spec/
|
|
241
|
+
contexts/
|
|
242
|
+
authenticated_request_context.rb
|
|
243
|
+
frozen_time_context.rb
|
|
244
|
+
contract_validation_context.rb
|
|
245
|
+
spec_helper.rb # requires all contexts
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
```ruby
|
|
249
|
+
# spec_helper.rb
|
|
250
|
+
Dir[File.join(__dir__, "contexts", "**", "*.rb")].each { |f| require f }
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Error Reference
|
|
254
|
+
|
|
255
|
+
### `RspecInContext::NoContextFound`
|
|
256
|
+
|
|
257
|
+
**Cause:** `in_context` references a name that doesn't exist or is out of scope.
|
|
258
|
+
|
|
259
|
+
**Fix:** Check spelling, ensure the context is defined globally or in an enclosing scope.
|
|
260
|
+
|
|
261
|
+
### `RspecInContext::AmbiguousContextName`
|
|
262
|
+
|
|
263
|
+
**Cause:** The same context name exists in multiple namespaces and no namespace was specified.
|
|
264
|
+
|
|
265
|
+
**Fix:** Add `ns: :your_namespace` to `in_context`.
|
|
266
|
+
|
|
267
|
+
### `RspecInContext::InvalidContextName`
|
|
268
|
+
|
|
269
|
+
**Cause:** `define_context` called with `nil` or `""`.
|
|
270
|
+
|
|
271
|
+
**Fix:** Provide a valid string or symbol name.
|
|
272
|
+
|
|
273
|
+
### `RspecInContext::MissingDefinitionBlock`
|
|
274
|
+
|
|
275
|
+
**Cause:** `define_context` called without a block.
|
|
276
|
+
|
|
277
|
+
**Fix:** Add a block: `define_context(:name) { ... }`.
|
|
278
|
+
|
|
279
|
+
## Anti-patterns / Gotchas
|
|
280
|
+
|
|
281
|
+
### Forgetting execute_tests when passing a block
|
|
282
|
+
|
|
283
|
+
```ruby
|
|
284
|
+
# BAD — the block passed to in_context is silently ignored
|
|
285
|
+
RSpec.define_context :setup do
|
|
286
|
+
let(:user) { create(:user) }
|
|
287
|
+
# missing execute_tests!
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
in_context :setup do
|
|
291
|
+
it "never runs" do # <-- this test is lost
|
|
292
|
+
expect(user).to be_present
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# GOOD
|
|
297
|
+
RSpec.define_context :setup do
|
|
298
|
+
let(:user) { create(:user) }
|
|
299
|
+
execute_tests
|
|
300
|
+
end
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Ambiguous context names without namespace
|
|
304
|
+
|
|
305
|
+
```ruby
|
|
306
|
+
# BAD — raises AmbiguousContextName
|
|
307
|
+
RSpec.define_context :valid_params, ns: :users do ... end
|
|
308
|
+
RSpec.define_context :valid_params, ns: :posts do ... end
|
|
309
|
+
in_context :valid_params do ... end # which one?
|
|
310
|
+
|
|
311
|
+
# GOOD
|
|
312
|
+
in_context :valid_params, ns: :users do ... end
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Using deprecated instanciate_context
|
|
316
|
+
|
|
317
|
+
```ruby
|
|
318
|
+
# DEPRECATED — emits warning
|
|
319
|
+
instanciate_context
|
|
320
|
+
|
|
321
|
+
# GOOD
|
|
322
|
+
execute_tests
|
|
323
|
+
# or
|
|
324
|
+
instantiate_context
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
## Real-World Examples
|
|
328
|
+
|
|
329
|
+
The `examples/` directory in the repository contains real-world usage patterns:
|
|
330
|
+
|
|
331
|
+
- `examples/contexts/` — Context definitions (authentication, interactor contracts, frozen time, job setup, mailer, composed contexts)
|
|
332
|
+
- `examples/usage/` — Spec files showing how to use those contexts in practice
|
|
333
|
+
|
|
334
|
+
See `examples/README.md` for the full list.
|
|
335
|
+
|
|
336
|
+
## Complete Example
|
|
337
|
+
|
|
338
|
+
Showing multiple features together:
|
|
339
|
+
|
|
340
|
+
```ruby
|
|
341
|
+
# spec/contexts/api_context.rb
|
|
342
|
+
RSpec.define_context :authenticated_api, silent: false do
|
|
343
|
+
let(:user) { create(:user) }
|
|
344
|
+
let(:headers) { { "Authorization" => "Bearer #{user.token}" } }
|
|
345
|
+
|
|
346
|
+
context "without authentication" do
|
|
347
|
+
let(:headers) { {} }
|
|
348
|
+
|
|
349
|
+
it "returns 401" do
|
|
350
|
+
send(http_method, endpoint, headers: headers)
|
|
351
|
+
expect(response).to have_http_status(:unauthorized)
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
execute_tests
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
RSpec.define_context :validates_contract do |required_fields|
|
|
359
|
+
required_fields.each do |field|
|
|
360
|
+
context "when #{field} is missing" do
|
|
361
|
+
let(field) { nil }
|
|
362
|
+
it("returns 422") { expect(response).to have_http_status(:unprocessable_entity) }
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# spec/requests/api/projects_spec.rb
|
|
368
|
+
RSpec.describe "API Projects", type: :request do
|
|
369
|
+
let(:http_method) { :post }
|
|
370
|
+
let(:endpoint) { api_projects_path }
|
|
371
|
+
|
|
372
|
+
in_context :authenticated_api do
|
|
373
|
+
let(:name) { "My Project" }
|
|
374
|
+
let(:description) { "A great project" }
|
|
375
|
+
|
|
376
|
+
before do
|
|
377
|
+
post endpoint, params: { name: name, description: description }, headers: headers
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
in_context :validates_contract, %i[name]
|
|
381
|
+
|
|
382
|
+
it "creates the project" do
|
|
383
|
+
expect(response).to have_http_status(:created)
|
|
384
|
+
expect(json_response["name"]).to eq("My Project")
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
```
|
data/README.md
CHANGED
|
@@ -3,9 +3,67 @@
|
|
|
3
3
|
[](https://badge.fury.io/rb/rspec_in_context)
|
|
4
4
|

|
|
5
5
|
|
|
6
|
-
This gem is here to help you write better shared_examples in
|
|
6
|
+
This gem is here to help you write better shared_examples in RSpec.
|
|
7
7
|
|
|
8
|
-
Ever been bothered by the fact that they don't really behave like methods and that you can't pass
|
|
8
|
+
Ever been bothered by the fact that they don't really behave like methods and that you can't pass them a block? There you go: `rspec_in_context`
|
|
9
|
+
|
|
10
|
+
## Why not just shared_examples?
|
|
11
|
+
|
|
12
|
+
`shared_examples` are great but they have a few limitations that can get annoying:
|
|
13
|
+
|
|
14
|
+
**You can't inject a block of tests at a specific point.** With `shared_examples`, your tests are either all inside the shared example or all outside. There's no way to say "set things up, run _these_ tests, then tear down". With `in_context`, you place `execute_tests` exactly where you want the caller's block to be injected.
|
|
15
|
+
|
|
16
|
+
**Composing them is awkward.** Nesting `it_behaves_like` inside another `shared_examples` works but reads poorly. `in_context` calls nest naturally, and you can use `in_context` inside a `define_context`.
|
|
17
|
+
|
|
18
|
+
**They don't accept arguments naturally.** `shared_examples` rely on `let` or params passed via `include_examples`. `in_context` accepts arguments directly, like a method call:
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
# shared_examples way
|
|
22
|
+
shared_examples "validates presence" do
|
|
23
|
+
it { is_expected.not_to be_valid }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
RSpec.describe User do
|
|
27
|
+
context "when email is nil" do
|
|
28
|
+
let(:email) { nil }
|
|
29
|
+
it_behaves_like "validates presence"
|
|
30
|
+
end
|
|
31
|
+
context "when name is nil" do
|
|
32
|
+
let(:name) { nil }
|
|
33
|
+
it_behaves_like "validates presence"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# in_context way
|
|
38
|
+
RSpec.define_context :validates_presence do |field|
|
|
39
|
+
context "when #{field} is nil" do
|
|
40
|
+
let(field) { nil }
|
|
41
|
+
it { is_expected.not_to be_valid }
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
RSpec.describe User do
|
|
46
|
+
in_context :validates_presence, :email
|
|
47
|
+
in_context :validates_presence, :name
|
|
48
|
+
end
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
In short: `in_context` makes reusable test blocks behave more like methods.
|
|
52
|
+
|
|
53
|
+
## Table of Contents
|
|
54
|
+
|
|
55
|
+
- [Why not just shared_examples?](#why-not-just-shared_examples)
|
|
56
|
+
- [Installation](#installation)
|
|
57
|
+
- [Usage](#usage)
|
|
58
|
+
- [Add this into RSpec](#add-this-into-rspec)
|
|
59
|
+
- [Define a new in_context](#define-a-new-in_context)
|
|
60
|
+
- [Use the context](#use-the-context)
|
|
61
|
+
- [Things to know](#things-to-know)
|
|
62
|
+
- [Errors](#errors)
|
|
63
|
+
- [Examples](#examples)
|
|
64
|
+
- [Migrating to 1.2.0](#migrating-to-120)
|
|
65
|
+
- [Development](#development)
|
|
66
|
+
- [Contributing](#contributing)
|
|
9
67
|
|
|
10
68
|
## Installation
|
|
11
69
|
|
|
@@ -25,28 +83,28 @@ Or install it yourself as:
|
|
|
25
83
|
|
|
26
84
|
## Usage
|
|
27
85
|
|
|
28
|
-
### Add this into
|
|
86
|
+
### Add this into RSpec
|
|
29
87
|
|
|
30
88
|
You must require the gem on top of your spec_helper:
|
|
31
89
|
```ruby
|
|
32
90
|
require 'rspec_in_context'
|
|
33
91
|
```
|
|
34
92
|
|
|
35
|
-
Then include it into
|
|
93
|
+
Then include it into RSpec:
|
|
36
94
|
```ruby
|
|
37
95
|
RSpec.configure do |config|
|
|
38
96
|
[...]
|
|
39
|
-
|
|
97
|
+
|
|
40
98
|
config.include RspecInContext
|
|
41
99
|
end
|
|
42
100
|
```
|
|
43
101
|
|
|
44
102
|
### Define a new in_context
|
|
45
103
|
|
|
46
|
-
You can define in_context
|
|
47
|
-
They completely look like normal
|
|
104
|
+
You can define in_context blocks that are reusable almost anywhere.
|
|
105
|
+
They completely look like normal RSpec.
|
|
48
106
|
|
|
49
|
-
##### Inside a
|
|
107
|
+
##### Inside a RSpec block (scoped)
|
|
50
108
|
|
|
51
109
|
```ruby
|
|
52
110
|
# A in_context can be named with a symbol or a string
|
|
@@ -59,19 +117,36 @@ end
|
|
|
59
117
|
|
|
60
118
|
Those in_context will be scoped to their current `describe`/`context` block.
|
|
61
119
|
|
|
62
|
-
##### Outside a
|
|
120
|
+
##### Outside a RSpec block (globally)
|
|
63
121
|
|
|
64
122
|
Outside of a test you have to use `RSpec.define_context`. Those in_context will be defined globally in your tests.
|
|
65
123
|
|
|
124
|
+
##### File organization
|
|
125
|
+
|
|
126
|
+
For global contexts, we recommend creating a `spec/contexts/` directory with one file per context (or per group of related contexts):
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
spec/
|
|
130
|
+
contexts/
|
|
131
|
+
authenticated_request_context.rb
|
|
132
|
+
frozen_time_context.rb
|
|
133
|
+
interactor_contract_context.rb
|
|
134
|
+
spec_helper.rb
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Then require them in your `spec_helper.rb`:
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
Dir[File.join(__dir__, "contexts", "**", "*.rb")].each { |f| require f }
|
|
141
|
+
```
|
|
66
142
|
|
|
67
143
|
### Use the context
|
|
68
144
|
|
|
69
145
|
Anywhere in your test description, use a `in_context` block to use a predefined in_context.
|
|
70
146
|
|
|
71
|
-
**Important**: in_context are scoped to their current `describe`/`context` block. If you need globally defined
|
|
147
|
+
**Important**: in_context are scoped to their current `describe`/`context` block. If you need globally defined contexts see `RSpec.define_context`
|
|
72
148
|
|
|
73
149
|
```ruby
|
|
74
|
-
# A in_context can be named with a symbol or a string
|
|
75
150
|
RSpec.define_context :context_name do
|
|
76
151
|
it 'works' do
|
|
77
152
|
expect(true).to be_truthy
|
|
@@ -79,7 +154,7 @@ RSpec.define_context :context_name do
|
|
|
79
154
|
end
|
|
80
155
|
|
|
81
156
|
[...]
|
|
82
|
-
|
|
157
|
+
RSpec.describe MyClass do
|
|
83
158
|
in_context :context_name # => will execute the 'it works' test here
|
|
84
159
|
end
|
|
85
160
|
```
|
|
@@ -88,30 +163,44 @@ end
|
|
|
88
163
|
|
|
89
164
|
#### Inside block execution
|
|
90
165
|
|
|
91
|
-
* You can
|
|
166
|
+
* You can choose exactly where your inside test will be used:
|
|
92
167
|
By using `execute_tests` in your define context, the test passed when you *use* the context will be executed here
|
|
93
168
|
|
|
94
169
|
```ruby
|
|
95
|
-
define_context :
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
170
|
+
RSpec.define_context :authenticated_request do
|
|
171
|
+
let(:user) { create(:user) }
|
|
172
|
+
|
|
173
|
+
before { sign_in user }
|
|
174
|
+
|
|
175
|
+
context "without authentication" do
|
|
176
|
+
before { sign_out user }
|
|
177
|
+
|
|
178
|
+
it "redirects to login" do
|
|
179
|
+
send(http_method, endpoint_path)
|
|
180
|
+
expect(response).to redirect_to(new_user_session_path)
|
|
181
|
+
end
|
|
103
182
|
end
|
|
183
|
+
|
|
184
|
+
execute_tests
|
|
104
185
|
end
|
|
105
186
|
|
|
106
187
|
[...]
|
|
107
188
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
189
|
+
RSpec.describe "Projects", type: :request do
|
|
190
|
+
let(:http_method) { :get }
|
|
191
|
+
let(:endpoint_path) { projects_path }
|
|
192
|
+
|
|
193
|
+
in_context :authenticated_request do
|
|
194
|
+
it "returns 200" do
|
|
195
|
+
get projects_path
|
|
196
|
+
expect(response).to have_http_status(:ok)
|
|
197
|
+
end
|
|
111
198
|
end
|
|
112
199
|
end
|
|
113
200
|
```
|
|
114
201
|
|
|
202
|
+
The block you pass to `in_context` gets injected exactly where `execute_tests` is placed. Setup, teardown, and built-in tests live together in the context definition. Your specific tests are injected right where they belong.
|
|
203
|
+
|
|
115
204
|
* You can add variable instantiation relative to your test where you exactly want:
|
|
116
205
|
|
|
117
206
|
`instantiate_context` is an alias of `execute_tests` so you can't use both.
|
|
@@ -121,63 +210,82 @@ But it lets you describe what the block will do better.
|
|
|
121
210
|
|
|
122
211
|
#### Variable usage
|
|
123
212
|
|
|
124
|
-
* You can use
|
|
213
|
+
* You can use variables in the in_context definition
|
|
125
214
|
|
|
126
215
|
```ruby
|
|
127
|
-
define_context :
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
216
|
+
RSpec.define_context :interactor_contract do |required_fields|
|
|
217
|
+
required_fields.each do |field|
|
|
218
|
+
context "when #{field} is missing" do
|
|
219
|
+
let(field) { nil }
|
|
220
|
+
|
|
221
|
+
it "fails" do
|
|
222
|
+
expect(subject).to be_a_failure
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
it "reports the breach" do
|
|
226
|
+
expect(subject.breaches).to include(field)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
135
229
|
end
|
|
136
230
|
end
|
|
137
231
|
|
|
138
232
|
[...]
|
|
139
233
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
234
|
+
RSpec.describe CreateInvoice do
|
|
235
|
+
subject { described_class.call(amount: amount, client: client) }
|
|
236
|
+
|
|
237
|
+
let(:amount) { 100 }
|
|
238
|
+
let(:client) { create(:client) }
|
|
239
|
+
|
|
240
|
+
in_context :interactor_contract, %i[amount client]
|
|
144
241
|
end
|
|
145
242
|
```
|
|
146
243
|
|
|
147
244
|
#### Scoping
|
|
148
245
|
|
|
149
|
-
* In_contexts can be
|
|
246
|
+
* In_contexts can be scoped inside one another
|
|
150
247
|
|
|
151
248
|
```ruby
|
|
152
|
-
define_context :
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
249
|
+
RSpec.define_context :with_frozen_time do
|
|
250
|
+
before { freeze_time }
|
|
251
|
+
execute_tests
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
RSpec.define_context :with_inline_mailer do
|
|
255
|
+
around do |example|
|
|
256
|
+
ActiveJob::Base.queue_adapter = :inline
|
|
257
|
+
ActionMailer::Base.deliveries.clear
|
|
258
|
+
example.run
|
|
259
|
+
ActionMailer::Base.deliveries.clear
|
|
260
|
+
ActiveJob::Base.queue_adapter = :test
|
|
160
261
|
end
|
|
262
|
+
execute_tests
|
|
161
263
|
end
|
|
162
264
|
|
|
163
|
-
|
|
164
|
-
context 'and tree also' do
|
|
165
|
-
let(:tree) { 'abcd' }
|
|
265
|
+
[...]
|
|
166
266
|
|
|
167
|
-
|
|
168
|
-
|
|
267
|
+
RSpec.describe PasswordReset do
|
|
268
|
+
in_context :with_frozen_time do
|
|
269
|
+
in_context :with_inline_mailer do
|
|
270
|
+
it "sends the reset email with correct timestamp" do
|
|
271
|
+
PasswordReset.call(user)
|
|
272
|
+
expect(ActionMailer::Base.deliveries.last.body)
|
|
273
|
+
.to include(Time.current.to_s)
|
|
274
|
+
end
|
|
169
275
|
end
|
|
170
276
|
end
|
|
171
277
|
end
|
|
278
|
+
```
|
|
172
279
|
|
|
173
|
-
|
|
280
|
+
* You can also use `in_context` inside a `define_context` to compose contexts together:
|
|
174
281
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
end
|
|
282
|
+
```ruby
|
|
283
|
+
RSpec.define_context :statistics_processor do
|
|
284
|
+
in_context :interactor_contract, %i[account date]
|
|
179
285
|
|
|
180
|
-
|
|
286
|
+
it "succeeds" do
|
|
287
|
+
expect(subject).to be_success
|
|
288
|
+
end
|
|
181
289
|
end
|
|
182
290
|
```
|
|
183
291
|
|
|
@@ -188,15 +296,15 @@ end
|
|
|
188
296
|
* You can add a namespace to a in_context definition
|
|
189
297
|
|
|
190
298
|
```ruby
|
|
191
|
-
define_context "
|
|
299
|
+
define_context "with valid params", namespace: "users"
|
|
192
300
|
```
|
|
193
301
|
Or
|
|
194
302
|
```ruby
|
|
195
|
-
define_context "
|
|
303
|
+
define_context "with valid params", ns: "users"
|
|
196
304
|
```
|
|
197
305
|
Or
|
|
198
306
|
```ruby
|
|
199
|
-
RSpec.define_context "
|
|
307
|
+
RSpec.define_context "with valid params", ns: "users"
|
|
200
308
|
```
|
|
201
309
|
|
|
202
310
|
* When you want to use a namespaced in_context, you have two choices:
|
|
@@ -220,12 +328,12 @@ in_context "namespaced context", namespace: "namespace name"
|
|
|
220
328
|
in_context "namespaced context", ns: "namespace name"
|
|
221
329
|
```
|
|
222
330
|
|
|
223
|
-
#### Making `in_context`
|
|
331
|
+
#### Making `in_context` advertise itself
|
|
224
332
|
|
|
225
333
|
The fact that a `in_context` block is used inside the test is silent and invisible by default.
|
|
226
|
-
`in_context` will still wrap its own execution inside
|
|
334
|
+
`in_context` will still wrap its own execution inside an anonymous context.
|
|
227
335
|
|
|
228
|
-
But, there's some
|
|
336
|
+
But, there's some cases where it helps to make the `in_context` wrap its execution in a named `context` block.
|
|
229
337
|
For example:
|
|
230
338
|
```ruby
|
|
231
339
|
define_context "with my_var defined" do
|
|
@@ -248,7 +356,7 @@ end
|
|
|
248
356
|
Using a `rspec -f doc` will only print "MyNiceClass works" and "MyNiceClass doesn't work" which is not really a good documentation.
|
|
249
357
|
|
|
250
358
|
So, you can define a context specifying it not to be `silent` or to `print_context`.
|
|
251
|
-
For example
|
|
359
|
+
For example:
|
|
252
360
|
```ruby
|
|
253
361
|
define_context "with my_var defined", silent: false do
|
|
254
362
|
before do
|
|
@@ -269,6 +377,36 @@ end
|
|
|
269
377
|
```
|
|
270
378
|
Will print "MyNiceClass with my_var defined works" and "MyNiceClass without my_var defined doesn't work". Which is valid and readable documentation.
|
|
271
379
|
|
|
380
|
+
#### Thread-safety & parallel_tests
|
|
381
|
+
|
|
382
|
+
The context registry is protected by a Mutex so it's safe to use with `parallel_tests` in thread mode.
|
|
383
|
+
|
|
384
|
+
#### Memory cleanup
|
|
385
|
+
|
|
386
|
+
For long-running test suites with many dynamically generated contexts, you can free all stored contexts:
|
|
387
|
+
|
|
388
|
+
```ruby
|
|
389
|
+
RspecInContext::InContext.clear_all_contexts!
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### Errors
|
|
393
|
+
|
|
394
|
+
| Error | Cause |
|
|
395
|
+
|---|---|
|
|
396
|
+
| `NoContextFound` | `in_context` refers to a name that doesn't exist or is out of scope |
|
|
397
|
+
| `AmbiguousContextName` | Same name exists in multiple namespaces, no namespace specified |
|
|
398
|
+
| `InvalidContextName` | `define_context` called with `nil` or empty name |
|
|
399
|
+
| `MissingDefinitionBlock` | `define_context` called without a block |
|
|
400
|
+
|
|
401
|
+
## Examples
|
|
402
|
+
|
|
403
|
+
The [`examples/`](examples/) directory contains real-world usage patterns:
|
|
404
|
+
|
|
405
|
+
- **`contexts/`** — Context definitions you'd put in `spec/contexts/` (authentication, interactor contracts, frozen time, job setup, mailer, composed contexts)
|
|
406
|
+
- **`usage/`** — Spec files showing how to use those contexts in practice
|
|
407
|
+
|
|
408
|
+
See [`examples/README.md`](examples/README.md) for the full list.
|
|
409
|
+
|
|
272
410
|
## Migrating to 1.2.0
|
|
273
411
|
|
|
274
412
|
### Breaking changes
|
|
@@ -289,10 +427,17 @@ Will print "MyNiceClass with my_var defined works" and "MyNiceClass without my_v
|
|
|
289
427
|
|
|
290
428
|
## Development
|
|
291
429
|
|
|
292
|
-
|
|
293
430
|
After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
294
431
|
|
|
295
|
-
After
|
|
432
|
+
After setting up the repo, you should run `overcommit --install` to install the different hooks.
|
|
433
|
+
|
|
434
|
+
Every commit/push is checked by overcommit.
|
|
435
|
+
|
|
436
|
+
Tool used in dev:
|
|
437
|
+
|
|
438
|
+
- RSpec
|
|
439
|
+
- Rubocop
|
|
440
|
+
- Prettier
|
|
296
441
|
|
|
297
442
|
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
298
443
|
|
data/examples/README.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Examples
|
|
2
|
+
|
|
3
|
+
Real-world usage examples for `rspec_in_context`.
|
|
4
|
+
|
|
5
|
+
## contexts/
|
|
6
|
+
|
|
7
|
+
Context definitions you'd put in `spec/contexts/` in a real project:
|
|
8
|
+
|
|
9
|
+
- **authenticated_request_context.rb** — Authentication setup with built-in unauthenticated test + `execute_tests` for your authenticated tests
|
|
10
|
+
- **interactor_contract_context.rb** — Parameterized contract validation for interactor-style service objects
|
|
11
|
+
- **frozen_time_context.rb** — Simple setup context using `freeze_time`
|
|
12
|
+
- **active_job_context.rb** — `around` hook to run jobs in test mode
|
|
13
|
+
- **inline_mailer_context.rb** — Delivers emails synchronously during tests
|
|
14
|
+
- **composed_context.rb** — Composing contexts: uses `in_context` inside `define_context`
|
|
15
|
+
|
|
16
|
+
## usage/
|
|
17
|
+
|
|
18
|
+
Spec files showing how to use the contexts above:
|
|
19
|
+
|
|
20
|
+
- **request_spec.rb** — Using `:authenticated_request` in a request spec
|
|
21
|
+
- **interactor_spec.rb** — Using `:interactor_expect` for contract validation
|
|
22
|
+
- **nested_contexts_spec.rb** — Nesting `:with_frozen_time` and `:with_inline_mailer`
|
|
23
|
+
- **composed_context_spec.rb** — Using `:service_processor` (which itself uses `:interactor_expect`)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Sets up ActiveJob in test mode for the duration of the tests.
|
|
2
|
+
# Useful when you need to assert on enqueued/performed jobs.
|
|
3
|
+
|
|
4
|
+
RSpec.define_context :with_test_jobs do
|
|
5
|
+
include ActiveJob::TestHelper
|
|
6
|
+
|
|
7
|
+
around do |example|
|
|
8
|
+
original_adapter = ActiveJob::Base.queue_adapter
|
|
9
|
+
ActiveJob::Base.queue_adapter = :test
|
|
10
|
+
example.run
|
|
11
|
+
ActiveJob::Base.queue_adapter = original_adapter
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
execute_tests
|
|
15
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# A context that sets up authentication, tests the unauthenticated case,
|
|
2
|
+
# then lets you inject your authenticated tests via execute_tests.
|
|
3
|
+
#
|
|
4
|
+
# Expects the caller to define:
|
|
5
|
+
# - http_method (e.g. :get, :post)
|
|
6
|
+
# - endpoint_path (e.g. users_path)
|
|
7
|
+
|
|
8
|
+
RSpec.define_context :authenticated_request do
|
|
9
|
+
let(:user) { create(:user) }
|
|
10
|
+
let(:account) { user.accounts.first }
|
|
11
|
+
|
|
12
|
+
before { sign_in user }
|
|
13
|
+
|
|
14
|
+
describe "authentication" do
|
|
15
|
+
context "without authentication" do
|
|
16
|
+
before { sign_out user }
|
|
17
|
+
|
|
18
|
+
it "redirects to login" do
|
|
19
|
+
send(http_method, endpoint_path)
|
|
20
|
+
|
|
21
|
+
expect(response).to redirect_to(new_user_session_path)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
execute_tests
|
|
27
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Example of composing contexts together.
|
|
2
|
+
# This context reuses :interactor_expect and adds a success test.
|
|
3
|
+
#
|
|
4
|
+
# Shows how in_context can be used inside define_context
|
|
5
|
+
# to build higher-level reusable blocks.
|
|
6
|
+
|
|
7
|
+
RSpec.define_context :service_processor do
|
|
8
|
+
in_context :interactor_expect, %i[account date]
|
|
9
|
+
|
|
10
|
+
it "succeeds with valid params" do
|
|
11
|
+
expect(subject).to be_success
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Delivers emails inline (synchronously) during tests.
|
|
2
|
+
# Clears the deliveries before and after each test.
|
|
3
|
+
|
|
4
|
+
RSpec.define_context :with_inline_mailer do
|
|
5
|
+
around do |example|
|
|
6
|
+
original_adapter = ActiveJob::Base.queue_adapter
|
|
7
|
+
ActiveJob::Base.queue_adapter = :inline
|
|
8
|
+
ActionMailer::Base.deliveries.clear
|
|
9
|
+
example.run
|
|
10
|
+
ActionMailer::Base.deliveries.clear
|
|
11
|
+
ActiveJob::Base.queue_adapter = original_adapter
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
execute_tests
|
|
15
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Validates that an interactor fails when required fields are missing.
|
|
2
|
+
# Pass the list of required fields as an argument.
|
|
3
|
+
#
|
|
4
|
+
# Expects the caller to define a subject that returns an interactor result,
|
|
5
|
+
# and a let for each required field.
|
|
6
|
+
|
|
7
|
+
RSpec.define_context :interactor_expect do |required_fields|
|
|
8
|
+
required_fields.each do |field|
|
|
9
|
+
context "when the field #{field} is not present" do
|
|
10
|
+
let(field) { nil }
|
|
11
|
+
|
|
12
|
+
it "fails" do
|
|
13
|
+
context = subject
|
|
14
|
+
expect(context).to be_a_failure
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "populates the breaches" do
|
|
18
|
+
context = subject
|
|
19
|
+
expect(context.breaches).to include(field)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Example: Using a composed context
|
|
2
|
+
#
|
|
3
|
+
# :service_processor reuses :interactor_expect internally,
|
|
4
|
+
# so you get contract validation + success test in one call.
|
|
5
|
+
|
|
6
|
+
require "rails_helper"
|
|
7
|
+
|
|
8
|
+
RSpec.describe DailyStatsProcessor do
|
|
9
|
+
subject { described_class.call(account: account, date: date) }
|
|
10
|
+
|
|
11
|
+
let(:account) { create(:account) }
|
|
12
|
+
let(:date) { Date.current }
|
|
13
|
+
|
|
14
|
+
in_context :service_processor
|
|
15
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Example: Using :interactor_expect for contract validation
|
|
2
|
+
#
|
|
3
|
+
# Tests that the interactor fails when any required field is nil,
|
|
4
|
+
# then tests the happy path separately.
|
|
5
|
+
|
|
6
|
+
require "rails_helper"
|
|
7
|
+
|
|
8
|
+
RSpec.describe CreateInvoice do
|
|
9
|
+
subject do
|
|
10
|
+
described_class.call(amount: amount, client: client, due_date: due_date)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
let(:amount) { 100 }
|
|
14
|
+
let(:client) { create(:client) }
|
|
15
|
+
let(:due_date) { Date.tomorrow }
|
|
16
|
+
|
|
17
|
+
in_context :interactor_expect, %i[amount client due_date]
|
|
18
|
+
|
|
19
|
+
it "creates the invoice" do
|
|
20
|
+
expect(subject).to be_success
|
|
21
|
+
expect(subject.invoice).to be_persisted
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Example: Nesting multiple contexts together
|
|
2
|
+
#
|
|
3
|
+
# Combines :with_frozen_time and :with_inline_mailer
|
|
4
|
+
# to test time-sensitive email delivery.
|
|
5
|
+
|
|
6
|
+
require "rails_helper"
|
|
7
|
+
|
|
8
|
+
RSpec.describe PasswordReset do
|
|
9
|
+
in_context :with_frozen_time do
|
|
10
|
+
in_context :with_inline_mailer do
|
|
11
|
+
it "sends the reset email" do
|
|
12
|
+
user = create(:user)
|
|
13
|
+
|
|
14
|
+
PasswordReset.call(user)
|
|
15
|
+
|
|
16
|
+
expect(ActionMailer::Base.deliveries.size).to eq(1)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "includes the current timestamp" do
|
|
20
|
+
user = create(:user)
|
|
21
|
+
|
|
22
|
+
PasswordReset.call(user)
|
|
23
|
+
|
|
24
|
+
expect(ActionMailer::Base.deliveries.last.body.to_s).to include(
|
|
25
|
+
Time.current.iso8601,
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Example: Using :authenticated_request in a request spec
|
|
2
|
+
#
|
|
3
|
+
# The context handles authentication setup and tests the unauthenticated case.
|
|
4
|
+
# Your tests inside the block run in an authenticated state.
|
|
5
|
+
|
|
6
|
+
require "rails_helper"
|
|
7
|
+
|
|
8
|
+
RSpec.describe "Projects", type: :request do
|
|
9
|
+
let(:http_method) { :get }
|
|
10
|
+
let(:endpoint_path) { projects_path }
|
|
11
|
+
|
|
12
|
+
in_context :authenticated_request do
|
|
13
|
+
describe "GET /projects" do
|
|
14
|
+
it "returns 200" do
|
|
15
|
+
get projects_path
|
|
16
|
+
|
|
17
|
+
expect(response).to have_http_status(:ok)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "lists the user's projects" do
|
|
21
|
+
project = create(:project, account: account)
|
|
22
|
+
|
|
23
|
+
get projects_path
|
|
24
|
+
|
|
25
|
+
expect(response.body).to include(project.name)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/rspec_in_context.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rspec_in_context
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.2.
|
|
4
|
+
version: 1.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Denis <Zaratan> Pasin
|
|
@@ -213,11 +213,23 @@ files:
|
|
|
213
213
|
- CHANGELOG.md
|
|
214
214
|
- Gemfile
|
|
215
215
|
- Guardfile
|
|
216
|
+
- LLMS.md
|
|
216
217
|
- README.md
|
|
217
218
|
- Rakefile
|
|
218
219
|
- TODO.md
|
|
219
220
|
- bin/console
|
|
220
221
|
- bin/setup
|
|
222
|
+
- examples/README.md
|
|
223
|
+
- examples/contexts/active_job_context.rb
|
|
224
|
+
- examples/contexts/authenticated_request_context.rb
|
|
225
|
+
- examples/contexts/composed_context.rb
|
|
226
|
+
- examples/contexts/frozen_time_context.rb
|
|
227
|
+
- examples/contexts/inline_mailer_context.rb
|
|
228
|
+
- examples/contexts/interactor_contract_context.rb
|
|
229
|
+
- examples/usage/composed_context_spec.rb
|
|
230
|
+
- examples/usage/interactor_spec.rb
|
|
231
|
+
- examples/usage/nested_contexts_spec.rb
|
|
232
|
+
- examples/usage/request_spec.rb
|
|
221
233
|
- lib/rspec_in_context.rb
|
|
222
234
|
- lib/rspec_in_context/context_management.rb
|
|
223
235
|
- lib/rspec_in_context/in_context.rb
|