rspec_power 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +503 -0
- data/Rakefile +20 -0
- data/lib/rspec_power/benchmark.rb +74 -0
- data/lib/rspec_power/ci.rb +21 -0
- data/lib/rspec_power/db_dump.rb +144 -0
- data/lib/rspec_power/engine.rb +5 -0
- data/lib/rspec_power/env.rb +30 -0
- data/lib/rspec_power/i18n.rb +21 -0
- data/lib/rspec_power/logging.rb +112 -0
- data/lib/rspec_power/performance.rb +32 -0
- data/lib/rspec_power/request_dump.rb +125 -0
- data/lib/rspec_power/sql.rb +105 -0
- data/lib/rspec_power/time.rb +37 -0
- data/lib/rspec_power/version.rb +3 -0
- data/lib/rspec_power.rb +61 -0
- metadata +127 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 15e38ae448c0c4dfeb0251c6186f5fff49aeb9fc36b68ee2c307e5bb9956a0e3
|
|
4
|
+
data.tar.gz: 29e94be5c241dcf1f839e5494c41d1327b289c8a6763a1cab3cda2ef4d601e81
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 5966690eb7da430b3e2ddd99acee303efb0c46909b6dca64d4d116fe12264d4e6fb40b215128850e4392f74fb152515e2dc2c667de2c8be52695467eaafe697f
|
|
7
|
+
data.tar.gz: 2b1c5ce8f74d29bbb54715aa8d4f9255a685f4e624073508e2fc2209d99a65bfcae6339d306a6bcc482641dc1325507781838a5c1dce7ed3b7d37b61128e8dac
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright Igor Kasyanchuk
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
# RSpec Power π₯
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/rb/rspec_power)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://www.railsjazz.com)
|
|
6
|
+
|
|
7
|
+
A powerful collection of RSpec helpers and utilities that supercharge your Rails testing experience! π
|
|
8
|
+
|
|
9
|
+
## β¨ Features
|
|
10
|
+
|
|
11
|
+
| Feature | Summary | Usage |
|
|
12
|
+
| --- | --- | --- |
|
|
13
|
+
| [π Enhanced Logging](#-enhanced-logging) | Capture and control Rails logs; ActiveRecord-only option | `:with_log`, `:with_logs`, `:with_log_ar`, `with_logging`, `with_ar_logging` |
|
|
14
|
+
| [π Environment Management](#-environment-variable-management) | Override environment variables with auto-restore | `:with_env`, `with_test_env` |
|
|
15
|
+
| [π I18n Testing](#-internationalization-i18n-testing) | Switch locales and assert translations | `:with_locale`, `with_locale` |
|
|
16
|
+
| [β° Time Freeze](#-time-freeze) | Freeze/travel time for deterministic tests | `:with_time_freeze` |
|
|
17
|
+
| [π Time Zone](#-time-zone) | Run examples in a specific time zone | `:with_time_zone` |
|
|
18
|
+
| [β‘ Performance Budgeting](#-performance-budgeting) | Enforce maximum example execution time | `with_maximum_execution_time`, `:with_maximum_execution_time` |
|
|
19
|
+
| [π Benchmarking](#-benchmarking) | Run examples multiple times and summarize | `with_benchmark: { runs: N }` |
|
|
20
|
+
| [π CI Guards](#-ci-guards) | Conditionally run or skip on CI | `:with_ci_only`, `:with_skip_ci` |
|
|
21
|
+
| [π§ͺ SQL Guards](#-sql-guards) | Ensure no SQL or require at least one | `expect_no_sql`, `:with_no_sql_queries`, `expect_sql`, `:with_sql_queries` |
|
|
22
|
+
| [πΎ Request Dump](#-request-dump) | Dump session, cookies, flash, headers after each example | `:with_request_dump`, `with_request_dump: { what: [:session, :cookies, :flash, :headers] }` |
|
|
23
|
+
| [π’ DB Dump on Failure](#-db-dump-on-failure) | Dump DB tables to CSV when an example fails | `:with_dump_db_on_fail`, `with_dump_db_on_fail: { tables: [...], except: [...] }` |
|
|
24
|
+
|
|
25
|
+
## π¦ Installation
|
|
26
|
+
|
|
27
|
+
Add this line to your application's Gemfile:
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
group :test do
|
|
31
|
+
gem "rspec_power", require: false
|
|
32
|
+
end
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
And then execute:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
bundle install
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## π Quick Start
|
|
42
|
+
|
|
43
|
+
The gem automatically configures itself when required. Just add it to your Gemfile.
|
|
44
|
+
|
|
45
|
+
## π Usage Examples
|
|
46
|
+
|
|
47
|
+
### π Enhanced Logging
|
|
48
|
+
|
|
49
|
+
Capture all Rails logs during specific tests:
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
RSpec.describe User, :with_log do
|
|
53
|
+
it "creates a user with logging" do
|
|
54
|
+
# All Rails logs will be captured and displayed
|
|
55
|
+
user = User.create!(name: "John Doe", email: "john@example.com")
|
|
56
|
+
expect(user).to be_persisted
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
or for specific tests (alias `:with_logs` is also supported):
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
RSpec.describe User do
|
|
65
|
+
it "creates a user with logging", :with_log do
|
|
66
|
+
# All Rails logs will be captured and displayed
|
|
67
|
+
user = User.create!(name: "John Doe", email: "john@example.com")
|
|
68
|
+
expect(user).to be_persisted
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Capture only ActiveRecord logs:
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
RSpec.describe User, :with_log_ar do
|
|
77
|
+
it "shows SQL queries" do
|
|
78
|
+
# Only ActiveRecord logs will be captured
|
|
79
|
+
users = User.where(active: true).includes(:profile)
|
|
80
|
+
expect(users).to be_any
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Manual logging control:
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
RSpec.describe User do
|
|
89
|
+
it "manually controls logging" do
|
|
90
|
+
with_logging do
|
|
91
|
+
# All Rails logs captured here
|
|
92
|
+
User.create!(name: "Jane")
|
|
93
|
+
end
|
|
94
|
+
# Logging back to normal here
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it "captures only ActiveRecord logs" do
|
|
98
|
+
with_ar_logging do
|
|
99
|
+
# Only ActiveRecord logs captured here
|
|
100
|
+
User.count
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### π§ͺ SQL Guards
|
|
107
|
+
|
|
108
|
+
Ensure a block performs no SQL:
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
RSpec.describe CacheWarmup do
|
|
112
|
+
it "does not hit the DB" do
|
|
113
|
+
expect_no_sql do
|
|
114
|
+
CacheWarmup.build_in_memory!
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Require that a block performs at least one SQL statement:
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
RSpec.describe MigrationChecker do
|
|
124
|
+
it "touches the DB" do
|
|
125
|
+
expect_sql do
|
|
126
|
+
ActiveRecord::Base.connection.execute("SELECT 1")
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Or tag an example/group to enforce or require queries automatically:
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
RSpec.describe CacheWarmup, :with_no_sql_queries do
|
|
136
|
+
it "builds entirely in memory" do
|
|
137
|
+
CacheWarmup.build_in_memory!
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
RSpec.describe MigrationChecker, :with_sql_queries do
|
|
142
|
+
it "must hit the DB at least once" do
|
|
143
|
+
ActiveRecord::Base.connection.execute("SELECT 1")
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
Supported tags: `:with_no_sql_queries`, `:with_sql_queries`
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### π Environment Variable Management
|
|
151
|
+
|
|
152
|
+
Override environment variables for specific tests:
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
RSpec.describe PaymentService, :with_env do
|
|
156
|
+
it "uses test API key", with_env: { 'STRIPE_API_KEY' => 'test_key_123' } do
|
|
157
|
+
service = PaymentService.new
|
|
158
|
+
expect(service.api_key).to eq('test_key_123')
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
it "handles multiple env vars", with_env: {
|
|
162
|
+
'RAILS_ENV' => 'test',
|
|
163
|
+
'DATABASE_URL' => 'postgresql://localhost/test_db'
|
|
164
|
+
} do
|
|
165
|
+
expect(ENV['RAILS_ENV']).to eq('test')
|
|
166
|
+
expect(ENV['DATABASE_URL']).to eq('postgresql://localhost/test_db')
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Manual environment control:
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
RSpec.describe ConfigService do
|
|
175
|
+
it "manually overrides environment" do
|
|
176
|
+
with_test_env('API_URL' => 'https://api.test.com') do
|
|
177
|
+
expect(ENV['API_URL']).to eq('https://api.test.com')
|
|
178
|
+
end
|
|
179
|
+
# Environment restored automatically
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### π Internationalization (I18n) Testing
|
|
185
|
+
|
|
186
|
+
Test your application in different locales:
|
|
187
|
+
|
|
188
|
+
```ruby
|
|
189
|
+
RSpec.describe User, :with_locale do
|
|
190
|
+
it "displays name in English", with_locale: :en do
|
|
191
|
+
user = User.new(name: "John")
|
|
192
|
+
expect(user.greeting).to eq("Hello, John!")
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
it "displays name in Spanish", with_locale: :es do
|
|
196
|
+
user = User.new(name: "Juan")
|
|
197
|
+
expect(user.greeting).to eq("Β‘Hola, Juan!")
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
it "displays name in French", with_locale: :fr do
|
|
201
|
+
user = User.new(name: "Jean")
|
|
202
|
+
expect(user.greeting).to eq("Bonjour, Jean!")
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Manual locale control:
|
|
208
|
+
|
|
209
|
+
```ruby
|
|
210
|
+
RSpec.describe LocalizationHelper do
|
|
211
|
+
it "manually changes locale" do
|
|
212
|
+
with_locale(:de) do
|
|
213
|
+
expect(I18n.locale).to eq(:de)
|
|
214
|
+
expect(t('hello')).to eq('Hallo')
|
|
215
|
+
end
|
|
216
|
+
# Locale restored automatically
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### β° Time Freeze
|
|
222
|
+
|
|
223
|
+
Freeze time for consistent test results:
|
|
224
|
+
|
|
225
|
+
```ruby
|
|
226
|
+
RSpec.describe Order, :with_time_freeze do
|
|
227
|
+
it "creates order with current timestamp", with_time_freeze: "2024-01-15 10:30:00" do
|
|
228
|
+
order = Order.create!(amount: 100)
|
|
229
|
+
expect(order.created_at).to eq(Time.parse("2024-01-15 10:30:00"))
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
it "handles time-sensitive logic", with_time_freeze: Time.new(2024, 12, 25, 12, 0, 0) do
|
|
233
|
+
expect(Time.current).to eq(Time.new(2024, 12, 25, 12, 0, 0))
|
|
234
|
+
# Test Christmas-specific logic
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### π Time Zone
|
|
240
|
+
|
|
241
|
+
```ruby
|
|
242
|
+
RSpec.describe ReportGenerator do
|
|
243
|
+
it "builds report in US Pacific", with_time_zone: "Pacific Time (US & Canada)" do
|
|
244
|
+
# The block runs with Time.zone set to Pacific
|
|
245
|
+
expect(Time.zone.name).to eq("Pacific Time (US & Canada)")
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### β‘ Performance Budgeting
|
|
251
|
+
|
|
252
|
+
Limit example duration:
|
|
253
|
+
|
|
254
|
+
```ruby
|
|
255
|
+
RSpec.describe Importer do
|
|
256
|
+
it "is fast enough" do
|
|
257
|
+
with_maximum_execution_time(50) do
|
|
258
|
+
Importer.run!
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Or via metadata:
|
|
265
|
+
|
|
266
|
+
```ruby
|
|
267
|
+
RSpec.describe Importer, with_maximum_execution_time: 100 do
|
|
268
|
+
it "completes quickly" do
|
|
269
|
+
Importer.run!
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### π Benchmarking
|
|
275
|
+
|
|
276
|
+
Benchmark entire examples via metadata and get a suite summary:
|
|
277
|
+
|
|
278
|
+
```ruby
|
|
279
|
+
RSpec.describe Parser, with_benchmark: { runs: 10 } do
|
|
280
|
+
it "parses quickly" do
|
|
281
|
+
Parser.parse!(payload)
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
The example is executed multiple times (runs) and the average/min/max times are printed after the suite.
|
|
287
|
+
|
|
288
|
+
### πΎ Request Dump
|
|
289
|
+
|
|
290
|
+
Dump request-related state after each example to help debug request specs.
|
|
291
|
+
|
|
292
|
+
Supported items:
|
|
293
|
+
|
|
294
|
+
- `:session`
|
|
295
|
+
- `:cookies`
|
|
296
|
+
- `:flash`
|
|
297
|
+
- `:headers`
|
|
298
|
+
|
|
299
|
+
Enable for an example or group and choose what to dump:
|
|
300
|
+
|
|
301
|
+
```ruby
|
|
302
|
+
RSpec.describe "Users API", type: :request do
|
|
303
|
+
it "dumps everything by default", :with_request_dump do
|
|
304
|
+
post "/set_state"
|
|
305
|
+
expect(response).to be_successful
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
it "dumps only session and cookies",
|
|
309
|
+
with_request_dump: { what: [:session, :cookies] } do
|
|
310
|
+
post "/set_state"
|
|
311
|
+
expect(response).to be_successful
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Example output:
|
|
317
|
+
|
|
318
|
+
```
|
|
319
|
+
[rspec_power] Dump after example: Users API dumps everything by default
|
|
320
|
+
[rspec_power] session: {"user_id"=>42}
|
|
321
|
+
[rspec_power] cookies: {"hello"=>"world"}
|
|
322
|
+
[rspec_power] flash: {"notice"=>"done"}
|
|
323
|
+
[rspec_power] headers: { ... }
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### π CI Guards
|
|
327
|
+
|
|
328
|
+
Run or skip specs depending on whether the suite is running on CI.
|
|
329
|
+
|
|
330
|
+
- Tag to run only on CI: `:with_ci_only`
|
|
331
|
+
- Tag to skip on CI: `:with_skip_ci`
|
|
332
|
+
|
|
333
|
+
```ruby
|
|
334
|
+
RSpec.describe Deployment, :with_ci_only do
|
|
335
|
+
it "runs only on CI" do
|
|
336
|
+
expect(ENV["CI"]).to be_present
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
RSpec.describe HeavySpec, :with_skip_ci do
|
|
341
|
+
it "skips on CI" do
|
|
342
|
+
# expensive checks
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
CI detection via environment variable:
|
|
348
|
+
|
|
349
|
+
The guards rely on the `CI` environment variable:
|
|
350
|
+
|
|
351
|
+
- Considered CI when `ENV["CI"]` is set to any non-empty value other than `"false"` or `"0"` (case-insensitive).
|
|
352
|
+
- Considered non-CI when `ENV["CI"]` is unset/empty, `"false"`, or `"0"`.
|
|
353
|
+
|
|
354
|
+
Examples:
|
|
355
|
+
|
|
356
|
+
```bash
|
|
357
|
+
# Run a single file as if on CI
|
|
358
|
+
CI=true bundle exec rspec spec/path/to/file_spec.rb
|
|
359
|
+
|
|
360
|
+
# Also treated as CI
|
|
361
|
+
CI=1 bundle exec rspec
|
|
362
|
+
|
|
363
|
+
# Explicitly run as non-CI
|
|
364
|
+
CI=0 bundle exec rspec
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### π’ DB Dump on Failure
|
|
368
|
+
|
|
369
|
+
Dump database state to CSV files when an example fails. Useful to inspect exactly what data led to the failure.
|
|
370
|
+
|
|
371
|
+
- Defaults:
|
|
372
|
+
- Dumps all non-empty tables, excluding `schema_migrations` and `ar_internal_metadata`
|
|
373
|
+
- Exports each table to a separate CSV, ordered by primary key (if present)
|
|
374
|
+
- Writes to `tmp/rspec_power/db_failures/<timestamp>_<spec-name>/`
|
|
375
|
+
- Includes `metadata.json` with spec info
|
|
376
|
+
|
|
377
|
+
Enable for an example or group:
|
|
378
|
+
|
|
379
|
+
```ruby
|
|
380
|
+
RSpec.describe User, :with_dump_db_on_fail do
|
|
381
|
+
it "creates a user" do
|
|
382
|
+
# ...
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
Customize which tables to include/exclude and output directory:
|
|
388
|
+
|
|
389
|
+
```ruby
|
|
390
|
+
RSpec.describe Report, with_dump_db_on_fail: {
|
|
391
|
+
tables: ["users", "accounts"],
|
|
392
|
+
except: ["accounts"],
|
|
393
|
+
dir: Rails.root.join("tmp", "db_dumps").to_s
|
|
394
|
+
} do
|
|
395
|
+
it "fails and dumps only selected tables" do
|
|
396
|
+
# ...
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
Options:
|
|
402
|
+
|
|
403
|
+
- `tables` / `only`: whitelist tables to dump
|
|
404
|
+
- `except` / `exclude`: tables to skip
|
|
405
|
+
- `dir`: base output directory (default: `tmp/rspec_power/db_failures`)
|
|
406
|
+
|
|
407
|
+
Compatibility: the legacy tag `:dump_db_on_fail` remains supported as an alias.
|
|
408
|
+
|
|
409
|
+
## π― Shared Contexts
|
|
410
|
+
|
|
411
|
+
The gem provides several pre-configured shared contexts:
|
|
412
|
+
|
|
413
|
+
- `rspec_power::logging:verbose` - Enables verbose logging for tests with `:with_log` metadata
|
|
414
|
+
- `rspec_power::logging:active_record` - Enables ActiveRecord logging for tests with `:with_log_ar` metadata
|
|
415
|
+
- `rspec_power::env:override` - Automatically handles environment variable overrides
|
|
416
|
+
- `rspec_power::i18n:dynamic` - Manages locale changes for tests with `:with_locale` metadata
|
|
417
|
+
- `rspec_power::time:freeze` - Handles time freezing for tests with `:with_time_freeze` metadata
|
|
418
|
+
- `rspec_power::time:zone` - Executes examples in a given time zone with `:with_time_zone` metadata
|
|
419
|
+
- `rspec_power::ci:only` - Runs examples only in CI when tagged with `:with_ci_only`
|
|
420
|
+
- `rspec_power::ci:skip` - Skips examples in CI when tagged with `:with_skip_ci`
|
|
421
|
+
- `rspec_power::request_dump:after` - Dumps selected request state after each example with `:with_request_dump` metadata
|
|
422
|
+
|
|
423
|
+
## π§ Configuration
|
|
424
|
+
|
|
425
|
+
The gem automatically configures itself, but you can customize the behavior:
|
|
426
|
+
|
|
427
|
+
```ruby
|
|
428
|
+
# In spec_helper.rb or rails_helper.rb
|
|
429
|
+
RSpec.configure do |config|
|
|
430
|
+
# Customize logging behavior
|
|
431
|
+
config.include RSpecPower::Rails::LoggingHelpers
|
|
432
|
+
config.include_context "rspec_power::logging:verbose", with_log: true
|
|
433
|
+
config.include_context "rspec_power::logging:verbose", with_logs: true
|
|
434
|
+
config.include_context "rspec_power::logging:active_record", with_log_ar: true
|
|
435
|
+
|
|
436
|
+
# Customize environment helpers
|
|
437
|
+
config.include RSpecPower::Rails::EnvHelpers
|
|
438
|
+
config.include_context "rspec_power::env:override", :with_env
|
|
439
|
+
|
|
440
|
+
# Customize I18n helpers
|
|
441
|
+
config.include RSpecPower::Rails::I18nHelpers
|
|
442
|
+
config.include_context "rspec_power::i18n:dynamic", :with_locale
|
|
443
|
+
|
|
444
|
+
# Customize time helpers
|
|
445
|
+
config.include RSpecPower::Rails::TimeHelpers
|
|
446
|
+
config.include_context "rspec_power::time:freeze", :with_time_freeze
|
|
447
|
+
config.include_context "rspec_power::time:zone", :with_time_zone
|
|
448
|
+
|
|
449
|
+
# CI-only guards
|
|
450
|
+
config.include_context "rspec_power::ci:only", :with_ci_only
|
|
451
|
+
config.include_context "rspec_power::ci:skip", :with_skip_ci
|
|
452
|
+
|
|
453
|
+
# Request dump helpers (session/cookies/flash/headers)
|
|
454
|
+
config.include RSpecPower::RequestDumpHelpers
|
|
455
|
+
config.include_context "rspec_power::request_dump:after", :with_request_dump
|
|
456
|
+
end
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
## π§ͺ Testing
|
|
460
|
+
|
|
461
|
+
Run the test suite:
|
|
462
|
+
|
|
463
|
+
```bash
|
|
464
|
+
bundle exec rspec
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
## Linter
|
|
468
|
+
|
|
469
|
+
```bash
|
|
470
|
+
bundle exec rubocop
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
To fix most issues, run:
|
|
474
|
+
|
|
475
|
+
```bash
|
|
476
|
+
bundle exec rubocop -A
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
## π€ Contributing
|
|
481
|
+
|
|
482
|
+
1. Fork the repository
|
|
483
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
484
|
+
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
|
485
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
486
|
+
5. Open a Pull Request
|
|
487
|
+
|
|
488
|
+
## π Credits
|
|
489
|
+
|
|
490
|
+
Code for logging was extracted from [test-prof](https://github.com/test-prof/test-prof) gem.
|
|
491
|
+
|
|
492
|
+
## π License
|
|
493
|
+
|
|
494
|
+
This project is licensed under the MIT License - see the [MIT-LICENSE](MIT-LICENSE) file for details.
|
|
495
|
+
|
|
496
|
+
---
|
|
497
|
+
|
|
498
|
+
[<img src="https://github.com/igorkasyanchuk/rails_time_travel/blob/main/docs/more_gems.png?raw=true"
|
|
499
|
+
/>](https://www.railsjazz.com/?utm_source=github&utm_medium=bottom&utm_campaign=rails_performance)
|
|
500
|
+
|
|
501
|
+
[](https://buymeacoffee.com/igorkasyanchuk)
|
|
502
|
+
|
|
503
|
+
Made with β€οΈ for the Rails testing community!
|
data/Rakefile
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
require "bundler/setup"
|
|
2
|
+
|
|
3
|
+
APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
|
|
4
|
+
|
|
5
|
+
require "bundler/gem_tasks"
|
|
6
|
+
|
|
7
|
+
require "rspec/core/rake_task"
|
|
8
|
+
|
|
9
|
+
# pluginβs own specs
|
|
10
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
11
|
+
|
|
12
|
+
# dummy app specs
|
|
13
|
+
namespace :dummy do
|
|
14
|
+
desc "Run dummy Rails app specs"
|
|
15
|
+
task :spec do
|
|
16
|
+
Dir.chdir("spec/dummy") { sh "bundle exec rspec" }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
task default: [ :spec, "dummy:spec" ]
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
module RSpecPower
|
|
2
|
+
module BenchmarkHelpers
|
|
3
|
+
class << self
|
|
4
|
+
def results_registry
|
|
5
|
+
@results_registry ||= []
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def add_result(result)
|
|
9
|
+
results_registry << result
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def results
|
|
13
|
+
results_registry.dup
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Internal: run the given block multiple times and record a summary.
|
|
18
|
+
# Used by the shared context, not exposed as a public helper anymore.
|
|
19
|
+
def __run_benchmark__(runs: 1, label:)
|
|
20
|
+
raise ArgumentError, "__run_benchmark__ requires a block" unless block_given?
|
|
21
|
+
|
|
22
|
+
iterations = runs.to_i
|
|
23
|
+
raise ArgumentError, "runs must be >= 1" if iterations < 1
|
|
24
|
+
|
|
25
|
+
timings_ms = []
|
|
26
|
+
iterations.times do
|
|
27
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
28
|
+
yield
|
|
29
|
+
finish = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
30
|
+
timings_ms << (finish - start) * 1000.0
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
avg = timings_ms.sum / timings_ms.length
|
|
34
|
+
summary = {
|
|
35
|
+
label: label,
|
|
36
|
+
runs: iterations,
|
|
37
|
+
avg_ms: avg,
|
|
38
|
+
min_ms: timings_ms.min,
|
|
39
|
+
max_ms: timings_ms.max
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
BenchmarkHelpers.add_result(summary)
|
|
43
|
+
summary
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Shared context to configure benchmark with metadata
|
|
49
|
+
RSpec.shared_context "rspec_power::benchmark:run" do
|
|
50
|
+
around(:each) do |example|
|
|
51
|
+
opts = example.metadata[:with_benchmark]
|
|
52
|
+
if opts
|
|
53
|
+
runs = (opts[:runs] || 1).to_i
|
|
54
|
+
label = example.full_description
|
|
55
|
+
extend RSpecPower::BenchmarkHelpers
|
|
56
|
+
__run_benchmark__(runs: runs, label: label) { example.run }
|
|
57
|
+
else
|
|
58
|
+
example.run
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Print a consolidated report after the suite finishes
|
|
64
|
+
RSpec.configure do |config|
|
|
65
|
+
config.after(:suite) do
|
|
66
|
+
results = RSpecPower::BenchmarkHelpers.results
|
|
67
|
+
next if results.empty?
|
|
68
|
+
|
|
69
|
+
puts "\nBenchmark results (rspec_power):"
|
|
70
|
+
results.each do |r|
|
|
71
|
+
puts "- #{r[:label]}: runs=#{r[:runs]} avg=#{format('%.3f', r[:avg_ms])}ms min=#{format('%.3f', r[:min_ms])}ms max=#{format('%.3f', r[:max_ms])}ms"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
RSpec.shared_context "rspec_power::ci:only" do
|
|
2
|
+
around(:each) do |example|
|
|
3
|
+
ci = ENV["CI"].to_s.downcase
|
|
4
|
+
if ci == "" || ci == "false" || ci == "0"
|
|
5
|
+
skip "Skipped in non-CI environment"
|
|
6
|
+
else
|
|
7
|
+
example.run
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
RSpec.shared_context "rspec_power::ci:skip" do
|
|
13
|
+
around(:each) do |example|
|
|
14
|
+
ci = ENV["CI"].to_s.downcase
|
|
15
|
+
if ci != "" && ci != "false" && ci != "0"
|
|
16
|
+
skip "Skipped in CI environment"
|
|
17
|
+
else
|
|
18
|
+
example.run
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|