rspec_in_context 1.1.0.3 → 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/.github/workflows/test_and_publish.yml +15 -10
- data/.github/workflows/test_only.yml +12 -7
- data/.github/workflows/verify_version_change.yml +6 -2
- data/.gitignore +7 -0
- data/.rubocop-remote-fc7fff8f41d19bde0dcc446ded612e75.yml +153 -0
- data/.rubocop.yml +12 -4
- data/.ruby-version +1 -1
- data/CHANGELOG.md +36 -1
- data/LLMS.md +388 -0
- data/README.md +237 -73
- data/TODO.md +44 -0
- 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 +1 -3
- data/lib/rspec_in_context/in_context.rb +137 -35
- data/lib/rspec_in_context/version.rb +1 -3
- data/lib/rspec_in_context.rb +13 -9
- data/package.json +16 -2
- data/rspec_in_context.gemspec +21 -20
- data/yarn.lock +8 -10
- metadata +28 -59
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
|
+
```
|