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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 730a3e4b4ffb390f1331ac01a19295c91c8c7d21b356e23618bc10d619645f2a
4
- data.tar.gz: 74b2c2d9f4f4921a7611d678dfc7202226bd123a670148564b6302d6f951a772
3
+ metadata.gz: e63cf4bd9f911ee7f0434682e2b104578555086eba35590ff0c8d72214ef2f48
4
+ data.tar.gz: d4737b49923e3a8080603987f4c490d19ed798f824ce0822dd0e349028683c79
5
5
  SHA512:
6
- metadata.gz: 21923024896432dbcd2e4234306df21ccb3e3ef8eddb3325c576322e5d54a8e54ad4169c2b94b9b3cb8b43ab78531a20f5dd61e9fa7653b6e54832eb05a84948
7
- data.tar.gz: 44badaca77e3c8994c0fc9faea35c8877d1d50c73bdee4a3caaeebd6497c336545c8e6ac3743c8b639fa4e6b1c1193a523ea5dd3beb4996e72eda6fccdbd9b3b
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.0...HEAD
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
  [![Gem Version](https://badge.fury.io/rb/rspec_in_context.svg)](https://badge.fury.io/rb/rspec_in_context)
4
4
  ![Test and Release badge](https://github.com/zaratan/rspec_in_context/workflows/Test%20and%20Release/badge.svg)
5
5
 
6
- This gem is here to help you write better shared_examples in Rspec.
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 it a block ? There you go: `rspec_in_context`
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 Rspec
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 Rspec:
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 block that are reusable almost anywhere.
47
- They completely look like normal Rspec.
104
+ You can define in_context blocks that are reusable almost anywhere.
105
+ They completely look like normal RSpec.
48
106
 
49
- ##### Inside a Rspec block (scoped)
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 Rspec block (globally)
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 context see `RSpec.define_context`
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
- Rspec.describe MyClass do
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 chose exactly where your inside test will be used:
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 :context_name do
96
- it 'works' do
97
- expect(true).to be_truthy
98
- end
99
- context "in this context pomme exists" do
100
- let(:pomme) { "abcd" }
101
-
102
- execute_tests
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
- in_context :context_name do
109
- it 'will be executed at execute_tests place' do
110
- expect(pomme).to eq("abcd") # => true
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 variable in the in_context definition
213
+ * You can use variables in the in_context definition
125
214
 
126
215
  ```ruby
127
- define_context :context_name do |name|
128
- it 'works' do
129
- expect(true).to be_truthy
130
- end
131
- context "in this context #{name} exists" do
132
- let(name) { "abcd" }
133
-
134
- execute_tests
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
- in_context :context_name, :poire do
141
- it 'the right variable will exists' do
142
- expect(poire).to eq("abcd") # => true
143
- end
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 scope inside one another
246
+ * In_contexts can be scoped inside one another
150
247
 
151
248
  ```ruby
152
- define_context :context_name do |name|
153
- it 'works' do
154
- expect(true).to be_truthy
155
- end
156
- context "in this context #{name} exists" do
157
- let(name) { "abcd" }
158
-
159
- execute_tests
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
- define_context "second in_context" do
164
- context 'and tree also' do
165
- let(:tree) { 'abcd' }
265
+ [...]
166
266
 
167
- it 'will scope correctly' do
168
- expect(tree).to eq(poire)
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
- in_context :context_name, :poire do
176
- it 'the right variable will exists' do
177
- expect(poire).to eq("abcd") # => true
178
- end
282
+ ```ruby
283
+ RSpec.define_context :statistics_processor do
284
+ in_context :interactor_contract, %i[account date]
179
285
 
180
- in_context "second in_context" # => will work
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 "this is a namespaced context", namespace: "namespace name"
299
+ define_context "with valid params", namespace: "users"
192
300
  ```
193
301
  Or
194
302
  ```ruby
195
- define_context "this is a namespaced context", ns: "namespace name"
303
+ define_context "with valid params", ns: "users"
196
304
  ```
197
305
  Or
198
306
  ```ruby
199
- RSpec.define_context "this is a namespaced context", ns: "namespace name"
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` adverstise itself
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 a anonymous context.
334
+ `in_context` will still wrap its own execution inside an anonymous context.
227
335
 
228
- But, there's some case where it helps to make the `in_context` to wrap its execution in a named `context` block.
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 setuping the repo, you should run `overcommit --install` to install the different hooks.
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
 
@@ -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,8 @@
1
+ # Freezes time for the duration of the tests.
2
+ # Uses ActiveSupport's freeze_time.
3
+
4
+ RSpec.define_context :with_frozen_time do
5
+ before { freeze_time }
6
+
7
+ execute_tests
8
+ 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
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  module RspecInContext
4
2
  # Allow context to be scoped inside a block
5
3
  module ContextManagement
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  # Base module
4
2
  module RspecInContext
5
3
  # Base error class for all gem errors
@@ -1,6 +1,4 @@
1
- # frozen_string_literal: true
2
-
3
1
  module RspecInContext
4
2
  # Version of the gem
5
- VERSION = "1.2.0"
3
+ VERSION = "1.2.1".freeze
6
4
  end
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  require "rspec_in_context/version"
4
2
  require "rspec_in_context/in_context"
5
3
  require "rspec_in_context/context_management"
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.0
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