spec_forge 0.4.0 → 0.5.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 +4 -4
- data/.standard.yml +4 -0
- data/CHANGELOG.md +40 -1
- data/README.md +31 -632
- data/lib/spec_forge/attribute/factory.rb +58 -6
- data/lib/spec_forge/attribute/faker.rb +4 -4
- data/lib/spec_forge/attribute/matcher.rb +4 -4
- data/lib/spec_forge/attribute/parameterized.rb +0 -8
- data/lib/spec_forge/attribute.rb +4 -2
- data/lib/spec_forge/configuration.rb +0 -3
- data/lib/spec_forge/core_ext/rspec.rb +2 -0
- data/lib/spec_forge/normalizer/constraint.rb +1 -1
- data/lib/spec_forge/normalizer/factory_reference.rb +5 -0
- data/lib/spec_forge/runner.rb +9 -4
- data/lib/spec_forge/spec/expectation/constraint.rb +19 -15
- data/lib/spec_forge/version.rb +1 -1
- data/spec_forge/forge_helper.rb +35 -24
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2f51faa712bafdcb48e3940273ec494eb3f39e43c0d122ff0a6f97e425026438
|
4
|
+
data.tar.gz: fe5cf7dff0a44cf5cfc466189fb38c3ed66da21637e18ebcdb44e31f9f6a8b73
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: be068681e3c2ce8b2f62fa21ae03281d40b34c19e190acce4f90796dd526197b6ae54cc6edaa04d6715e347a67e2c193e2b99b0e16e45b47b3183915001790f3
|
7
|
+
data.tar.gz: 644f2c1a49d3ed45a201cb53d731176a418c134679f324ef96f331427481643b2744bd6c27c0f326df32c1db751c412137ad209a3f0b0ce3add9cc68eb6a660e
|
data/.standard.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file.
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
7
|
|
8
|
+
<!--
|
8
9
|
## [Unreleased]
|
9
10
|
|
10
11
|
### Added
|
@@ -12,6 +13,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
12
13
|
### Changed
|
13
14
|
|
14
15
|
### Removed
|
16
|
+
-->
|
17
|
+
|
18
|
+
## [Unreleased]
|
19
|
+
|
20
|
+
### Added
|
21
|
+
|
22
|
+
### Changed
|
23
|
+
|
24
|
+
### Removed
|
25
|
+
|
26
|
+
## [0.5.0] - 12025-02-28
|
27
|
+
|
28
|
+
### Added
|
29
|
+
|
30
|
+
- Added support for testing array responses via `json`
|
31
|
+
- Added check to block RSpec overwrites from running with internal tests
|
32
|
+
- Added debugging access to `expected_json_class` variable.
|
33
|
+
- Added support for FactoryBot list strategies through the new `size` attribute
|
34
|
+
```yaml
|
35
|
+
variables:
|
36
|
+
users:
|
37
|
+
factory.user:
|
38
|
+
size: 10 # Creates 10 user records
|
39
|
+
```
|
40
|
+
- All FactoryBot list methods now supported:
|
41
|
+
- `create_list` (default)
|
42
|
+
- `build_list`
|
43
|
+
- `build_stubbed_list`
|
44
|
+
- `attributes_for_list`
|
45
|
+
- `build_pair`
|
46
|
+
- `create_pair`
|
47
|
+
- Comprehensive documentation available in the [Factory Lists wiki](https://github.com/itsthedevman/spec_forge/wiki/Factory-Lists)
|
48
|
+
|
49
|
+
### Changed
|
50
|
+
|
51
|
+
- Updated `Constraint` to use `include` for testing Hashes and `contains_exactly` for testing Arrays
|
52
|
+
- Better handling of positional and keyword argument passing for `Matcher` and `Faker` attributes
|
15
53
|
|
16
54
|
## [0.4.0] - 12025-02-22
|
17
55
|
|
@@ -88,7 +126,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
88
126
|
|
89
127
|
- Initial commit
|
90
128
|
|
91
|
-
[unreleased]: https://github.com/itsthedevman/spec_forge/compare/v0.
|
129
|
+
[unreleased]: https://github.com/itsthedevman/spec_forge/compare/v0.5.0...HEAD
|
130
|
+
[0.5.0]: https://github.com/itsthedevman/spec_forge/compare/v0.4.0...v0.5.0
|
92
131
|
[0.4.0]: https://github.com/itsthedevman/spec_forge/compare/v0.3.2...v0.4.0
|
93
132
|
[0.3.2]: https://github.com/itsthedevman/spec_forge/compare/v0.3.0...v0.3.2
|
94
133
|
[0.3.0]: https://github.com/itsthedevman/spec_forge/compare/v0.2.0...v0.3.0
|
data/README.md
CHANGED
@@ -23,11 +23,11 @@ That's a complete test. No Ruby code, no configuration files, no HTTP client set
|
|
23
23
|
|
24
24
|
SpecForge shines when you need:
|
25
25
|
|
26
|
-
1. **
|
27
|
-
2. **
|
28
|
-
3. **
|
29
|
-
4. **
|
30
|
-
5. **
|
26
|
+
1. **Living Documentation**: Tests serve as clear, maintainable documentation of your API's expected behavior.
|
27
|
+
2. **Power Without Complexity**: Get the benefits of Ruby-based tests (dynamic data, factories, matchers) without writing Ruby code.
|
28
|
+
3. **Quick Setup**: Start testing APIs without configuring HTTP clients or writing boilerplate code.
|
29
|
+
4. **Gradual Adoption**: Use alongside your existing test suite. Keep complex tests in RSpec while making simple API tests more accessible.
|
30
|
+
5. **Accessible API Testing**: Non-developers can write and maintain tests without Ruby knowledge. The YAML syntax reads like documentation.
|
31
31
|
|
32
32
|
## When Not to Use SpecForge
|
33
33
|
|
@@ -38,75 +38,6 @@ Consider alternatives when you need:
|
|
38
38
|
3. **Custom Response Validation**: For validation logic beyond what matchers provide.
|
39
39
|
4. **Complex Non-JSON Testing**: While SpecForge handles basic XML/HTML responses (coming soon), complex validation might need specialized tools.
|
40
40
|
|
41
|
-
## Roadmap
|
42
|
-
|
43
|
-
Current development priorities:
|
44
|
-
- [ ] Array support for `json` expectations
|
45
|
-
- [ ] Negated matchers: `matcher.not`
|
46
|
-
- [ ] `create_list/build_list` factory strategies
|
47
|
-
- [ ] `transform.map` support
|
48
|
-
- [ ] XML/HTML response handling
|
49
|
-
- [ ] OpenAPI generation from tests
|
50
|
-
- [x] Support for running individual specs
|
51
|
-
- [x] Improved error handling
|
52
|
-
|
53
|
-
Have a feature request? Open an issue on GitHub!
|
54
|
-
|
55
|
-
## Looking for a Software Engineer?
|
56
|
-
|
57
|
-
I'm currently looking for opportunities where I can tackle meaningful problems and help build reliable software while mentoring the next generation of developers. If you're looking for a senior engineer with full-stack Rails expertise and a passion for clean, maintainable code, let's talk!
|
58
|
-
|
59
|
-
[bryan@itsthedevman.com](mailto:bryan@itsthedevman.com)
|
60
|
-
|
61
|
-
## Table of Contents
|
62
|
-
|
63
|
-
- [Compatibility](#compatibility)
|
64
|
-
- [Installation](#installation)
|
65
|
-
- [Getting Started](#getting-started)
|
66
|
-
- [Forging Your First Test](#forging-your-first-test)
|
67
|
-
- [Running Tests](#running-tests)
|
68
|
-
- [Targeting Specific Files](#targeting-specific-files)
|
69
|
-
- [Targeting Specific Specs](#targeting-specific-specs)
|
70
|
-
- [Targeting Individual Expectations](#targeting-individual-expectations)
|
71
|
-
- [Configuration](#configuration)
|
72
|
-
- [Basic Configuration](#basic-configuration)
|
73
|
-
- [Framework Integration](#framework-integration)
|
74
|
-
- [Factory Configuration](#factory-configuration)
|
75
|
-
- [Debug Configuration](#debug-configuration)
|
76
|
-
- [Test Framework Configuration](#test-framework-configuration)
|
77
|
-
- [Configuration Inheritance](#configuration-inheritance)
|
78
|
-
- [Writing Tests](#writing-tests)
|
79
|
-
- [Basic Structure](#basic-structure)
|
80
|
-
- [Testing Response Data](#testing-response-data)
|
81
|
-
- [Multiple Expectations](#multiple-expectations)
|
82
|
-
- [Request Data](#request-data)
|
83
|
-
- [Path Parameters](#path-parameters)
|
84
|
-
- [Dynamic Features](#dynamic-features)
|
85
|
-
- [Variables](#variables)
|
86
|
-
- [Transformations](#transformations)
|
87
|
-
- [Chaining Support](#chaining-support)
|
88
|
-
- [Factory Support](#factory-support)
|
89
|
-
- [Automatic Discovery](#automatic-discovery)
|
90
|
-
- [Custom Factory Paths](#custom-factory-paths)
|
91
|
-
- [Build Strategies](#build-strategies)
|
92
|
-
- [YAML Factory Definitions](#yaml-factory-definitions)
|
93
|
-
- [RSpec Matchers](#rspec-matchers)
|
94
|
-
- ["be" namespace](#be-namespace)
|
95
|
-
- ["kind_of" namespace](#kind_of-namespace)
|
96
|
-
- ["matchers" namespace](#matchers-namespace)
|
97
|
-
- [How Tests Work](#how-tests-work)
|
98
|
-
- [Contributing](#contributing)
|
99
|
-
- [License](#license)
|
100
|
-
- [Credits](#credits)
|
101
|
-
|
102
|
-
Also see: [API Documentation](https://itsthedevman.com/docs/spec_forge)
|
103
|
-
|
104
|
-
## Compatibility
|
105
|
-
|
106
|
-
Currently tested on:
|
107
|
-
- MRI Ruby 3.2+
|
108
|
-
- NixOS (see `flake.nix` for details)
|
109
|
-
|
110
41
|
## Installation
|
111
42
|
|
112
43
|
Add this line to your application's Gemfile:
|
@@ -135,587 +66,55 @@ Initialize the required directory structure:
|
|
135
66
|
spec_forge init
|
136
67
|
```
|
137
68
|
|
138
|
-
|
139
|
-
```bash
|
140
|
-
bundle exec spec_forge init
|
141
|
-
```
|
142
|
-
|
143
|
-
This creates the `spec_forge` directory containing factory definitions, test specifications, and global configuration.
|
144
|
-
|
145
|
-
## Forging Your First Test
|
146
|
-
|
147
|
-
Let's write a simple test to verify a user endpoint. Create a new spec file:
|
69
|
+
Create your first test:
|
148
70
|
|
149
71
|
```bash
|
150
72
|
spec_forge new spec users
|
151
73
|
```
|
152
74
|
|
153
|
-
|
154
|
-
|
155
|
-
```yaml
|
156
|
-
get_user:
|
157
|
-
path: /users/1
|
158
|
-
method: GET
|
159
|
-
expectations:
|
160
|
-
- expect:
|
161
|
-
status: 200
|
162
|
-
json:
|
163
|
-
id: 1
|
164
|
-
name: kind_of.string
|
165
|
-
email: /@/
|
166
|
-
```
|
167
|
-
|
168
|
-
Run your tests with:
|
75
|
+
Run your tests:
|
169
76
|
|
170
77
|
```bash
|
171
78
|
spec_forge run
|
172
79
|
```
|
173
80
|
|
174
|
-
|
175
|
-
|
176
|
-
```bash
|
177
|
-
spec_forge
|
178
|
-
```
|
179
|
-
|
180
|
-
## Running Tests
|
181
|
-
|
182
|
-
As your test suite grows, you'll want more control over which tests to run.
|
183
|
-
|
184
|
-
#### Targeting Specific Files
|
185
|
-
|
186
|
-
When working on a specific feature, run tests from a single file:
|
187
|
-
|
188
|
-
```bash
|
189
|
-
spec_forge users # Runs all tests in specs/users.yml
|
190
|
-
```
|
191
|
-
|
192
|
-
#### Targeting Specific Specs
|
193
|
-
|
194
|
-
Focus on a specific endpoint by running a single spec:
|
195
|
-
|
196
|
-
```bash
|
197
|
-
spec_forge users:destroy_user # Runs all expectations in the destroy_user spec
|
198
|
-
```
|
199
|
-
|
200
|
-
#### Targeting Individual Expectations
|
201
|
-
|
202
|
-
You can also run individual expectations within a spec. The format depends on whether the expectation has a name:
|
203
|
-
|
204
|
-
```yaml
|
205
|
-
# specs/users.yml
|
206
|
-
destroy_user:
|
207
|
-
path: /users/:id
|
208
|
-
method: delete
|
209
|
-
expectations:
|
210
|
-
- expect: # Unnamed expectation
|
211
|
-
status: 200
|
212
|
-
- name: "Destroys a User" # Named expectation
|
213
|
-
expect:
|
214
|
-
status: 200
|
215
|
-
```
|
216
|
-
|
217
|
-
For named expectations:
|
218
|
-
```bash
|
219
|
-
# Format: <file>:<spec>:'<verb> <path> - <name>'
|
220
|
-
spec_forge users:destroy_user:'DELETE /users/:id - Destroys a User'
|
221
|
-
```
|
222
|
-
|
223
|
-
For unnamed expectations:
|
224
|
-
```bash
|
225
|
-
# Format: <file>:<spec>:'<verb> <path>'
|
226
|
-
spec_forge users:destroy_user:'DELETE /users/:id'
|
227
|
-
```
|
228
|
-
|
229
|
-
**Note**: When targeting an unnamed expectation, SpecForge executes all matching expectations within that spec. This means if you have multiple unnamed expectations with the same verb and path, they will all run.
|
230
|
-
|
231
|
-
## Configuration
|
232
|
-
|
233
|
-
### Basic Configuration
|
234
|
-
|
235
|
-
When you initialize SpecForge, it creates a `forge_helper.rb` file in your `spec_forge` directory. This serves as your central configuration file:
|
236
|
-
|
237
|
-
```ruby
|
238
|
-
SpecForge.configure do |config|
|
239
|
-
# Base URL for all requests
|
240
|
-
config.base_url = "http://localhost:3000"
|
241
|
-
|
242
|
-
# Default headers sent with every request
|
243
|
-
config.headers = {
|
244
|
-
"Authorization" => "Bearer #{ENV.fetch("API_TOKEN", "")}",
|
245
|
-
"Accept" => "application/json"
|
246
|
-
}
|
247
|
-
|
248
|
-
# Optional: Default query parameters for all requests
|
249
|
-
config.query = {
|
250
|
-
api_key: ENV["API_KEY"]
|
251
|
-
}
|
252
|
-
end
|
253
|
-
```
|
254
|
-
|
255
|
-
### Framework Integration
|
256
|
-
|
257
|
-
SpecForge works seamlessly with Rails and RSpec:
|
258
|
-
|
259
|
-
```ruby
|
260
|
-
# Rails Integration
|
261
|
-
require_relative "../config/environment"
|
262
|
-
|
263
|
-
# RSpec Integration (includes your existing configurations)
|
264
|
-
require_relative "../spec/spec_helper"
|
265
|
-
|
266
|
-
# Load custom files (models, libraries, etc)
|
267
|
-
Dir[File.join(__dir__, "..", "lib", "**", "*.rb")].sort.each { |f| require f }
|
268
|
-
```
|
269
|
-
|
270
|
-
### Factory Configuration
|
271
|
-
|
272
|
-
SpecForge provides flexible configuration options for working with FactoryBot factories:
|
273
|
-
|
274
|
-
```ruby
|
275
|
-
SpecForge.configure do |config|
|
276
|
-
# Disable auto-discovery if needed (default: true)
|
277
|
-
config.factories.auto_discover = false
|
278
|
-
|
279
|
-
# Add custom factory paths (appends to default paths)
|
280
|
-
config.factories.paths += ["lib/factories"]
|
281
|
-
end
|
282
|
-
```
|
283
|
-
|
284
|
-
### Debug Configuration
|
285
|
-
|
286
|
-
Enable debugging by adding `debug: true` (aliases: `breakpoint`, `pry`) at either the spec or expectation level:
|
287
|
-
|
288
|
-
```ruby
|
289
|
-
SpecForge.configure do |config|
|
290
|
-
# Custom debug handler (defaults to printing state overview)
|
291
|
-
config.on_debug { binding.pry } # Requires 'pry' gem
|
292
|
-
end
|
293
|
-
```
|
294
|
-
|
295
|
-
```yaml
|
296
|
-
get_users:
|
297
|
-
debug: true # Debug all expectations in this spec
|
298
|
-
path: /users
|
299
|
-
expectations:
|
300
|
-
- expect:
|
301
|
-
status: 200
|
302
|
-
- debug: true # Debug just this expectation
|
303
|
-
expect:
|
304
|
-
status: 404
|
305
|
-
json:
|
306
|
-
error: kind_of.string
|
307
|
-
```
|
308
|
-
|
309
|
-
When debugging, you have access to:
|
310
|
-
- `expectation` - Current expectation being validated
|
311
|
-
- `variables` - Resolved variables for the current expectation
|
312
|
-
- `request` - Request details (url, method, headers, etc.)
|
313
|
-
- `response` - Full response including headers, status, and parsed body
|
314
|
-
- `expected_status` - Expected HTTP status code
|
315
|
-
- `expected_json` - Expected JSON structure with matchers
|
316
|
-
|
317
|
-
Or call `self` from an interactive session to see everything as a hash
|
318
|
-
|
319
|
-
### Test Framework Configuration
|
320
|
-
|
321
|
-
Access RSpec's configuration through the `specs` attribute:
|
322
|
-
|
323
|
-
```ruby
|
324
|
-
SpecForge.configure do |config|
|
325
|
-
# Setup before all tests
|
326
|
-
config.specs.before(:suite) do
|
327
|
-
DatabaseCleaner.strategy = :truncation
|
328
|
-
DatabaseCleaner.clean_with(:truncation)
|
329
|
-
end
|
330
|
-
|
331
|
-
# Wrap each test
|
332
|
-
config.specs.around do |example|
|
333
|
-
DatabaseCleaner.cleaning do
|
334
|
-
example.run
|
335
|
-
end
|
336
|
-
end
|
337
|
-
end
|
338
|
-
```
|
339
|
-
|
340
|
-
### Configuration Inheritance
|
341
|
-
|
342
|
-
All configuration options can be overridden at three levels (in order of precedence):
|
343
|
-
|
344
|
-
1. Individual expectation
|
345
|
-
2. Spec level
|
346
|
-
3. Global configuration (forge_helper.rb)
|
347
|
-
|
348
|
-
For example:
|
349
|
-
|
350
|
-
```yaml
|
351
|
-
# Override at spec level
|
352
|
-
get_user:
|
353
|
-
base_url: https://staging.example.com
|
354
|
-
headers:
|
355
|
-
x_custom_header: "overridden" # Underscore keys automatically convert to "X-Custom-Header"
|
356
|
-
|
357
|
-
expectations:
|
358
|
-
# Override for a specific expectation
|
359
|
-
- base_url: https://prod.example.com
|
360
|
-
headers:
|
361
|
-
X-Custom-Header: "expectation-specific" # HTTP-style headers used as-is
|
362
|
-
expect:
|
363
|
-
status: 200
|
364
|
-
```
|
365
|
-
|
366
|
-
## Writing Tests
|
367
|
-
|
368
|
-
### Basic Structure
|
369
|
-
|
370
|
-
Every spec needs a path, HTTP method, and at least one expectation:
|
371
|
-
|
372
|
-
```yaml
|
373
|
-
show_user:
|
374
|
-
path: /users/1
|
375
|
-
method: GET # Optional for GET requests
|
376
|
-
expectations:
|
377
|
-
- expect:
|
378
|
-
status: 200
|
379
|
-
```
|
380
|
-
|
381
|
-
### Testing Response Data
|
382
|
-
|
383
|
-
Verify the response JSON:
|
384
|
-
|
385
|
-
```yaml
|
386
|
-
show_user:
|
387
|
-
path: /users/1
|
388
|
-
expectations:
|
389
|
-
- expect:
|
390
|
-
status: 200
|
391
|
-
json:
|
392
|
-
id: 1
|
393
|
-
name: kind_of.string
|
394
|
-
role: admin
|
395
|
-
```
|
396
|
-
|
397
|
-
### Multiple Expectations
|
398
|
-
|
399
|
-
Each expectation can override any spec-level setting:
|
400
|
-
|
401
|
-
```yaml
|
402
|
-
show_user:
|
403
|
-
path: /users/1
|
404
|
-
expectations:
|
405
|
-
- expect:
|
406
|
-
status: 200
|
407
|
-
json:
|
408
|
-
id: 1
|
409
|
-
role: admin
|
410
|
-
- path: /users/999 # Overrides spec-level path
|
411
|
-
expect:
|
412
|
-
status: 404
|
413
|
-
```
|
414
|
-
|
415
|
-
### Request Data
|
416
|
-
|
417
|
-
Add query parameters and body data:
|
418
|
-
|
419
|
-
```yaml
|
420
|
-
create_user:
|
421
|
-
path: /users
|
422
|
-
method: POST
|
423
|
-
query: # or "params" if you prefer
|
424
|
-
team_id: 123
|
425
|
-
body: # or "data" if you prefer
|
426
|
-
name: John Doe
|
427
|
-
email: john@example.com
|
428
|
-
expectations:
|
429
|
-
- expect:
|
430
|
-
status: 201
|
431
|
-
```
|
81
|
+
## Documentation
|
432
82
|
|
433
|
-
|
83
|
+
For comprehensive documentation, visit the [SpecForge Wiki](https://github.com/itsthedevman/spec_forge/wiki) which includes:
|
434
84
|
|
435
|
-
|
85
|
+
- [Getting Started Guide](https://github.com/itsthedevman/spec_forge/wiki/Getting-Started)
|
86
|
+
- [Configuration Options](https://github.com/itsthedevman/spec_forge/wiki/Configuration)
|
87
|
+
- [Writing Tests](https://github.com/itsthedevman/spec_forge/wiki/Writing-Tests)
|
88
|
+
- [Dynamic Features](https://github.com/itsthedevman/spec_forge/wiki/Dynamic-Features)
|
89
|
+
- [Factory Support](https://github.com/itsthedevman/spec_forge/wiki/Factory-Support)
|
90
|
+
- [RSpec Matchers](https://github.com/itsthedevman/spec_forge/wiki/RSpec-Matchers)
|
436
91
|
|
437
|
-
|
438
|
-
show_user:
|
439
|
-
path: /users/{id} # Use {id} or :id
|
440
|
-
query:
|
441
|
-
id: 1 # Replaces the placeholder
|
442
|
-
expectations:
|
443
|
-
- expect:
|
444
|
-
status: 200
|
445
|
-
```
|
446
|
-
|
447
|
-
## Dynamic Features
|
448
|
-
|
449
|
-
### Variables
|
450
|
-
|
451
|
-
Variables let you define and reuse values:
|
452
|
-
|
453
|
-
```yaml
|
454
|
-
list_posts:
|
455
|
-
variables:
|
456
|
-
author: factories.user
|
457
|
-
category_name: faker.lorem.word
|
458
|
-
query:
|
459
|
-
author_id: variables.author.id
|
460
|
-
category: variables.category_name
|
461
|
-
expectations:
|
462
|
-
- expect:
|
463
|
-
status: 200
|
464
|
-
json:
|
465
|
-
posts:
|
466
|
-
matcher.include:
|
467
|
-
- author:
|
468
|
-
id: variables.author.id
|
469
|
-
name: variables.author.name
|
470
|
-
category: variables.category_name
|
471
|
-
```
|
472
|
-
|
473
|
-
### Transformations
|
474
|
-
|
475
|
-
Transform data using built-in helpers:
|
476
|
-
|
477
|
-
```yaml
|
478
|
-
create_user:
|
479
|
-
variables:
|
480
|
-
first_name: faker.name.first_name
|
481
|
-
last_name: faker.name.last_name
|
482
|
-
full_name:
|
483
|
-
transform.join:
|
484
|
-
- variables.first_name
|
485
|
-
- " "
|
486
|
-
- variables.last_name
|
487
|
-
body:
|
488
|
-
name: variables.full_name
|
489
|
-
email: faker.internet.email
|
490
|
-
```
|
491
|
-
|
492
|
-
### Chaining
|
493
|
-
|
494
|
-
Access nested attributes and methods through chaining:
|
495
|
-
|
496
|
-
```yaml
|
497
|
-
list_posts:
|
498
|
-
variables:
|
499
|
-
# Factory chaining examples
|
500
|
-
owner: factories.user # Creates a user
|
501
|
-
name: factories.user.name # Gets just the name
|
502
|
-
company: variables.owner.company # Access factory attributes
|
503
|
-
|
504
|
-
# Variable chaining for relationships
|
505
|
-
first_post: variables.user.posts.first
|
506
|
-
|
507
|
-
# You can use array indices directly
|
508
|
-
comment_author: variables.first_post.comments.2.author.name
|
509
|
-
|
510
|
-
# Faker method chaining
|
511
|
-
lowercase_email: faker.internet.email.downcase
|
512
|
-
title_name: faker.name.first_name.titleize
|
513
|
-
```
|
514
|
-
|
515
|
-
### Factory Build Strategies
|
516
|
-
|
517
|
-
Control how factories create objects and customize their attributes:
|
518
|
-
|
519
|
-
```yaml
|
520
|
-
create_user:
|
521
|
-
variables:
|
522
|
-
# Default strategy (create)
|
523
|
-
regular_user: factories.user
|
524
|
-
|
525
|
-
# Custom build strategy and attributes
|
526
|
-
custom_user:
|
527
|
-
factory.user:
|
528
|
-
strategy: build # 'create' (default) or 'build'
|
529
|
-
attributes:
|
530
|
-
name: "Custom Name"
|
531
|
-
email: faker.internet.email
|
532
|
-
```
|
92
|
+
Also see the [API Documentation](https://itsthedevman.com/docs/spec_forge).
|
533
93
|
|
534
|
-
##
|
535
|
-
|
536
|
-
### Automatic Discovery
|
537
|
-
|
538
|
-
SpecForge automatically discovers factories in standard paths:
|
539
|
-
|
540
|
-
```ruby
|
541
|
-
SpecForge.configure do |config|
|
542
|
-
# Disable automatic factory discovery if needed (default: true)
|
543
|
-
config.factories.auto_discover = false
|
544
|
-
end
|
545
|
-
```
|
546
|
-
|
547
|
-
### Custom Factory Paths
|
548
|
-
|
549
|
-
Add custom paths to the factory search list:
|
550
|
-
|
551
|
-
```ruby
|
552
|
-
SpecForge.configure do |config|
|
553
|
-
# Add custom factory paths (appends to default paths)
|
554
|
-
# Ignored if `auto_discovery` is false
|
555
|
-
config.factories.paths += ["lib/factories"]
|
556
|
-
end
|
557
|
-
```
|
558
|
-
|
559
|
-
### Factory Build Strategies
|
560
|
-
|
561
|
-
Control how factories create objects and customize their attributes:
|
562
|
-
|
563
|
-
```yaml
|
564
|
-
create_user:
|
565
|
-
variables:
|
566
|
-
# Default strategy (create)
|
567
|
-
regular_user: factories.user
|
568
|
-
|
569
|
-
# Custom build strategy and attributes
|
570
|
-
custom_user:
|
571
|
-
factory.user:
|
572
|
-
strategy: build # 'create' (default) or 'build'
|
573
|
-
attributes:
|
574
|
-
name: "Custom Name"
|
575
|
-
email: faker.internet.email
|
576
|
-
```
|
577
|
-
|
578
|
-
### YAML Factory Definitions
|
579
|
-
|
580
|
-
Define factories in YAML with a simple declarative syntax:
|
581
|
-
|
582
|
-
```yaml
|
583
|
-
# spec_forge/factories/user.yml
|
584
|
-
user:
|
585
|
-
class: User # Optional model class name
|
586
|
-
variables:
|
587
|
-
department: faker.company.department
|
588
|
-
team_size:
|
589
|
-
faker.number.between:
|
590
|
-
from: 5
|
591
|
-
to: 20
|
592
|
-
attributes:
|
593
|
-
name: faker.name.name
|
594
|
-
email: faker.internet.email
|
595
|
-
role: admin
|
596
|
-
department: variables.department
|
597
|
-
team_count: variables.team_size
|
598
|
-
```
|
599
|
-
|
600
|
-
## RSpec Matchers
|
601
|
-
|
602
|
-
### "be" namespace
|
603
|
-
|
604
|
-
```yaml
|
605
|
-
expect:
|
606
|
-
json:
|
607
|
-
# Simple predicates
|
608
|
-
active: be.true
|
609
|
-
deleted: be.false
|
610
|
-
description: be.nil
|
611
|
-
tags: be.empty
|
612
|
-
email: be.present
|
613
|
-
|
614
|
-
# Comparisons
|
615
|
-
price:
|
616
|
-
be.greater_than: 18
|
617
|
-
stock:
|
618
|
-
be.less_than_or_equal: 100
|
619
|
-
rating:
|
620
|
-
be.between:
|
621
|
-
- 1
|
622
|
-
- 5
|
623
|
-
|
624
|
-
# Dynamic predicate methods
|
625
|
-
published: be.published
|
626
|
-
admin: be.admin
|
627
|
-
```
|
628
|
-
|
629
|
-
### "kind_of" namespace
|
630
|
-
|
631
|
-
```yaml
|
632
|
-
expect:
|
633
|
-
json:
|
634
|
-
id: kind_of.integer
|
635
|
-
name: kind_of.string
|
636
|
-
metadata: kind_of.hash
|
637
|
-
scores: kind_of.array
|
638
|
-
```
|
639
|
-
|
640
|
-
### "matchers" namespace
|
641
|
-
|
642
|
-
```yaml
|
643
|
-
expect:
|
644
|
-
json:
|
645
|
-
tags:
|
646
|
-
matcher.include:
|
647
|
-
- featured
|
648
|
-
- published
|
649
|
-
|
650
|
-
slug: /^[a-z0-9-]+$/ # Shorthand for matching regexes
|
651
|
-
|
652
|
-
config:
|
653
|
-
matcher.have_key: api_version
|
654
|
-
```
|
655
|
-
|
656
|
-
## How Tests Work
|
657
|
-
|
658
|
-
When you write a YAML spec, SpecForge converts it into an RSpec test structure. For example, this YAML:
|
659
|
-
|
660
|
-
```yaml
|
661
|
-
create_user:
|
662
|
-
path: /users
|
663
|
-
method: POST
|
664
|
-
variables:
|
665
|
-
full_name: faker.name.name
|
666
|
-
body:
|
667
|
-
name: variables.full_name
|
668
|
-
expectations:
|
669
|
-
- expect:
|
670
|
-
status: 201
|
671
|
-
json:
|
672
|
-
name: variables.full_name
|
673
|
-
```
|
94
|
+
## Roadmap
|
674
95
|
|
675
|
-
|
96
|
+
Current development priorities:
|
97
|
+
- [ ] Negated matchers: `matcher.not`
|
98
|
+
- [ ] `create_list/build_list` factory strategies
|
99
|
+
- [ ] `transform.map` support
|
100
|
+
- [ ] XML/HTML response handling
|
101
|
+
- [ ] OpenAPI generation from tests
|
102
|
+
- [x] Array support for `json` expectations
|
103
|
+
- [x] Support for running individual specs
|
104
|
+
- [x] Improved error handling
|
676
105
|
|
677
|
-
|
678
|
-
RSpec.describe "create_user" do
|
679
|
-
describe "POST /users" do
|
680
|
-
let(:full_name) { Faker::Name.name }
|
681
|
-
|
682
|
-
let!(:expected_status) { 201 }
|
683
|
-
let!(:expected_json) do
|
684
|
-
{
|
685
|
-
name: eq(full_name)
|
686
|
-
}
|
687
|
-
end
|
688
|
-
|
689
|
-
subject(:response) do
|
690
|
-
post("/users", body: { name: full_name })
|
691
|
-
end
|
692
|
-
|
693
|
-
it do
|
694
|
-
expect(response.status).to eq(expected_status)
|
695
|
-
expect(response.body).to include(expected_json)
|
696
|
-
end
|
697
|
-
end
|
698
|
-
end
|
699
|
-
```
|
106
|
+
Have a feature request? Open an issue on GitHub!
|
700
107
|
|
701
108
|
## Contributing
|
702
109
|
|
703
|
-
|
704
|
-
2. Create your feature branch (`git checkout -b feature/my-new-feature`)
|
705
|
-
3. Commit your changes (`git commit -am 'Add some feature'`)
|
706
|
-
4. Push to the branch (`git push origin feature/my-new-feature`)
|
707
|
-
5. Create new Pull Request
|
708
|
-
|
709
|
-
Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms.
|
110
|
+
Contributions are welcome! See the [Contributing Guide](https://github.com/itsthedevman/spec_forge/wiki/Contributing) for details on how to get started.
|
710
111
|
|
711
112
|
## License
|
712
113
|
|
713
114
|
The gem is available as open source under the terms of the [MIT License](LICENSE.txt).
|
714
115
|
|
715
|
-
##
|
716
|
-
|
717
|
-
See [CHANGELOG.md](CHANGELOG.md) for a list of changes.
|
116
|
+
## Looking for a Software Engineer?
|
718
117
|
|
719
|
-
|
118
|
+
I'm currently looking for opportunities where I can tackle meaningful problems and help build reliable software while mentoring the next generation of developers. If you're looking for a senior engineer with full-stack Rails expertise and a passion for clean, maintainable code, let's talk!
|
720
119
|
|
721
|
-
|
120
|
+
[bryan@itsthedevman.com](mailto:bryan@itsthedevman.com)
|
@@ -7,11 +7,27 @@ module SpecForge
|
|
7
7
|
|
8
8
|
KEYWORD_REGEX = /^factories\./i
|
9
9
|
|
10
|
-
|
10
|
+
# These are the base strategies that can be provided either with or without size
|
11
|
+
# stubbed will be transformed into build_stubbed
|
12
|
+
BASE_STRATEGIES = %w[
|
11
13
|
build
|
12
14
|
create
|
15
|
+
build_stubbed
|
13
16
|
attributes_for
|
17
|
+
].freeze
|
18
|
+
|
19
|
+
# All available build strategies that are accepted
|
20
|
+
BUILD_STRATEGIES = %w[
|
21
|
+
attributes_for
|
22
|
+
attributes_for_list
|
23
|
+
build
|
24
|
+
build_list
|
25
|
+
build_pair
|
14
26
|
build_stubbed
|
27
|
+
build_stubbed_list
|
28
|
+
create
|
29
|
+
create_list
|
30
|
+
create_pair
|
15
31
|
].freeze
|
16
32
|
|
17
33
|
alias_method :factory_name, :header
|
@@ -34,17 +50,53 @@ module SpecForge
|
|
34
50
|
|
35
51
|
def base_object
|
36
52
|
attributes = arguments[:keyword]
|
53
|
+
|
54
|
+
# Default functionality is to create ("factory.user")
|
37
55
|
return FactoryBot.create(factory_name) if attributes.blank?
|
38
56
|
|
39
|
-
|
40
|
-
|
57
|
+
build_arguments = construct_factory_parameters(attributes)
|
58
|
+
FactoryBot.public_send(*build_arguments)
|
59
|
+
end
|
60
|
+
|
61
|
+
def construct_factory_parameters(attributes)
|
62
|
+
build_strategy, list_size = determine_build_strategy(attributes)
|
63
|
+
|
64
|
+
# This is set up for the base strategies + _pair
|
65
|
+
# FactoryBot.create(factory_name, **attributes)
|
66
|
+
build_arguments = [
|
67
|
+
build_strategy,
|
68
|
+
factory_name,
|
69
|
+
**attributes[:attributes].resolve_value
|
70
|
+
]
|
71
|
+
|
72
|
+
# Insert the list size after the strategy
|
73
|
+
# FactoryBot.create_list(factory_name, list_size, **attributes)
|
74
|
+
if build_strategy.end_with?("_list")
|
75
|
+
build_arguments.insert(2, list_size)
|
76
|
+
end
|
77
|
+
|
78
|
+
build_arguments
|
79
|
+
end
|
80
|
+
|
81
|
+
def determine_build_strategy(attributes)
|
82
|
+
# Determine build strat, and unfreeze
|
83
|
+
build_strategy = +attributes[:build_strategy].resolve_value
|
84
|
+
list_size = attributes[:size].resolve_value
|
41
85
|
|
42
86
|
# stubbed => build_stubbed
|
43
|
-
build_strategy.prepend("build_") if build_strategy
|
87
|
+
build_strategy.prepend("build_") if build_strategy.start_with?("stubbed")
|
88
|
+
|
89
|
+
# create + size => create_list
|
90
|
+
# build + size => build_list
|
91
|
+
# build_stubbed + size => build_stubbed_list
|
92
|
+
# attributes_for + size => attributes_for_list
|
93
|
+
if list_size.positive? && BASE_STRATEGIES.include?(build_strategy)
|
94
|
+
build_strategy += "_list"
|
95
|
+
end
|
96
|
+
|
44
97
|
raise InvalidBuildStrategy, build_strategy unless BUILD_STRATEGIES.include?(build_strategy)
|
45
98
|
|
46
|
-
|
47
|
-
FactoryBot.public_send(build_strategy, factory_name, **attributes)
|
99
|
+
[build_strategy, list_size]
|
48
100
|
end
|
49
101
|
end
|
50
102
|
end
|
@@ -25,10 +25,10 @@ module SpecForge
|
|
25
25
|
private
|
26
26
|
|
27
27
|
def base_object
|
28
|
-
if
|
29
|
-
faker_method.call(*
|
30
|
-
elsif
|
31
|
-
faker_method.call(**
|
28
|
+
if (positional = arguments[:positional]) && positional.present?
|
29
|
+
faker_method.call(*positional.resolve)
|
30
|
+
elsif (keyword = arguments[:keyword]) && keyword.present?
|
31
|
+
faker_method.call(**keyword.resolve)
|
32
32
|
else
|
33
33
|
faker_method.call
|
34
34
|
end
|
@@ -44,14 +44,14 @@ module SpecForge
|
|
44
44
|
end
|
45
45
|
|
46
46
|
def value
|
47
|
-
if
|
48
|
-
positional =
|
47
|
+
if (positional = arguments[:positional]) && positional.present?
|
48
|
+
positional = positional.resolve.each do |value|
|
49
49
|
value.deep_stringify_keys! if value.respond_to?(:deep_stringify_keys!)
|
50
50
|
end
|
51
51
|
|
52
52
|
matcher_method.call(*positional)
|
53
|
-
elsif
|
54
|
-
matcher_method.call(**
|
53
|
+
elsif (keyword = arguments[:keyword]) && keyword.present?
|
54
|
+
matcher_method.call(**keyword.resolve.deep_stringify_keys)
|
55
55
|
else
|
56
56
|
matcher_method.call
|
57
57
|
end
|
@@ -63,14 +63,6 @@ module SpecForge
|
|
63
63
|
def prepare_arguments!
|
64
64
|
@arguments = Attribute.from(arguments)
|
65
65
|
end
|
66
|
-
|
67
|
-
def uses_positional_arguments?(method)
|
68
|
-
method.parameters.any? { |a| [:req, :opt, :rest].include?(a.first) }
|
69
|
-
end
|
70
|
-
|
71
|
-
def uses_keyword_arguments?(method)
|
72
|
-
method.parameters.any? { |a| [:keyreq, :key, :keyrest].include?(a.first) }
|
73
|
-
end
|
74
66
|
end
|
75
67
|
end
|
76
68
|
end
|
data/lib/spec_forge/attribute.rb
CHANGED
@@ -18,6 +18,8 @@ require_relative "attribute/variable"
|
|
18
18
|
|
19
19
|
module SpecForge
|
20
20
|
class Attribute
|
21
|
+
include Resolvable
|
22
|
+
|
21
23
|
#
|
22
24
|
# Binds variables to Attribute objects
|
23
25
|
#
|
@@ -191,9 +193,9 @@ module SpecForge
|
|
191
193
|
def __resolve(value)
|
192
194
|
case value
|
193
195
|
when ArrayLike
|
194
|
-
value.map(
|
196
|
+
value.map(&resolvable_proc)
|
195
197
|
when HashLike
|
196
|
-
value.transform_values(
|
198
|
+
value.transform_values(&resolvable_proc)
|
197
199
|
else
|
198
200
|
value
|
199
201
|
end
|
@@ -30,9 +30,6 @@ module SpecForge
|
|
30
30
|
def initialize
|
31
31
|
config = Normalizer.default_configuration
|
32
32
|
|
33
|
-
# Allows me to modify the error backtrace reporting within rspec
|
34
|
-
RSpec.configuration.instance_variable_set(:@backtrace_formatter, BacktraceFormatter)
|
35
|
-
|
36
33
|
config[:base_url] = "http://localhost:3000"
|
37
34
|
config[:factories] = Factories.new
|
38
35
|
config[:specs] = RSpec.configuration
|
data/lib/spec_forge/runner.rb
CHANGED
@@ -7,6 +7,9 @@ module SpecForge
|
|
7
7
|
# Runs any specs
|
8
8
|
#
|
9
9
|
def run
|
10
|
+
# Allows me to modify the error backtrace reporting within rspec
|
11
|
+
RSpec.configuration.instance_variable_set(:@backtrace_formatter, BacktraceFormatter)
|
12
|
+
|
10
13
|
RSpec::Core::Runner.disable_autorun!
|
11
14
|
RSpec::Core::Runner.run([], $stderr, $stdout)
|
12
15
|
end
|
@@ -29,7 +32,8 @@ module SpecForge
|
|
29
32
|
constraints = expectation.constraints
|
30
33
|
|
31
34
|
let!(:expected_status) { constraints.status.resolve }
|
32
|
-
let!(:expected_json) { constraints.json.resolve
|
35
|
+
let!(:expected_json) { constraints.json.resolve }
|
36
|
+
let!(:expected_json_class) { expected_json&.expected.class }
|
33
37
|
|
34
38
|
before do
|
35
39
|
# Ensure all variables are called and resolved, in case they are not referenced
|
@@ -50,9 +54,9 @@ module SpecForge
|
|
50
54
|
expect(response.status).to eq(expected_status)
|
51
55
|
|
52
56
|
# JSON check
|
53
|
-
if
|
54
|
-
expect(response.body).to be_kind_of(
|
55
|
-
expect(response.body).to
|
57
|
+
if expected_json
|
58
|
+
expect(response.body).to be_kind_of(expected_json_class)
|
59
|
+
expect(response.body).to expected_json
|
56
60
|
end
|
57
61
|
end
|
58
62
|
end
|
@@ -119,6 +123,7 @@ module SpecForge
|
|
119
123
|
- variables: Current variable definitions
|
120
124
|
- expected_status: Expected HTTP status code (#{expected_status})
|
121
125
|
- expected_json: Expected response body
|
126
|
+
- expected_json_class: Expected response body class
|
122
127
|
- request: HTTP request details (method, url, headers, body)
|
123
128
|
- response: HTTP response
|
124
129
|
|
@@ -11,10 +11,10 @@ module SpecForge
|
|
11
11
|
# Creates a new Constraint
|
12
12
|
#
|
13
13
|
# @param status [Integer] The expected HTTP status code
|
14
|
-
# @param json [Hash] The expected JSON with matchers
|
14
|
+
# @param json [Hash, Array] The expected JSON with matchers
|
15
15
|
#
|
16
16
|
def initialize(status:, json:)
|
17
|
-
super(status:, json:
|
17
|
+
super(status:, json: convert_to_matchers(json))
|
18
18
|
end
|
19
19
|
|
20
20
|
def to_h
|
@@ -23,20 +23,24 @@ module SpecForge
|
|
23
23
|
|
24
24
|
private
|
25
25
|
|
26
|
-
def
|
27
|
-
|
28
|
-
|
29
|
-
case attribute
|
30
|
-
when Attribute::Regex
|
31
|
-
Attribute.from("matcher.match" => attribute.resolve)
|
32
|
-
when Attribute::Literal
|
33
|
-
Attribute.from("matcher.eq" => attribute.resolve)
|
34
|
-
else
|
35
|
-
attribute
|
36
|
-
end
|
37
|
-
end
|
26
|
+
def convert_to_matchers(value)
|
27
|
+
# This makes it easier to check if json was provided
|
28
|
+
return Attribute.from(nil) if value.blank?
|
38
29
|
|
39
|
-
|
30
|
+
case value
|
31
|
+
when HashLike
|
32
|
+
value = value.transform_values { |i| convert_to_matchers(i) }
|
33
|
+
Attribute.from("matcher.include" => value)
|
34
|
+
when ArrayLike
|
35
|
+
value = value.map { |i| convert_to_matchers(i) }
|
36
|
+
Attribute.from("matcher.contain_exactly" => value)
|
37
|
+
when Attribute::Regex
|
38
|
+
Attribute.from("matcher.match" => value)
|
39
|
+
when Attribute::Literal
|
40
|
+
Attribute.from("matcher.eq" => value)
|
41
|
+
else
|
42
|
+
value
|
43
|
+
end
|
40
44
|
end
|
41
45
|
end
|
42
46
|
end
|
data/lib/spec_forge/version.rb
CHANGED
data/spec_forge/forge_helper.rb
CHANGED
@@ -1,37 +1,48 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
#
|
5
|
-
|
3
|
+
##########################################
|
4
|
+
# Framework Integration
|
5
|
+
##########################################
|
6
6
|
|
7
|
-
|
8
|
-
#
|
7
|
+
# Rails Integration
|
8
|
+
# require_relative "../config/environment"
|
9
9
|
|
10
|
-
|
10
|
+
# RSpec Integration (includes your existing configurations)
|
11
11
|
# require_relative "../spec/spec_helper"
|
12
12
|
|
13
|
+
# Custom requires (models, libraries, etc)
|
14
|
+
# Dir[File.join(__dir__, "..", "lib", "**", "*.rb")].sort.each { |f| require f }
|
15
|
+
|
16
|
+
##########################################
|
17
|
+
# Configuration
|
18
|
+
##########################################
|
19
|
+
|
13
20
|
SpecForge.configure do |config|
|
14
|
-
|
21
|
+
# Base configuration
|
15
22
|
config.base_url = "http://localhost:3000"
|
16
23
|
|
17
|
-
|
18
|
-
api_token = ENV.fetch("API_TOKEN", "")
|
24
|
+
# Default request headers
|
19
25
|
config.headers = {
|
20
|
-
"Authorization" => "Bearer #{
|
26
|
+
"Authorization" => "Bearer #{ENV.fetch("API_TOKEN", "")}"
|
21
27
|
}
|
22
28
|
|
23
|
-
|
24
|
-
# config.query = {
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
#
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
29
|
+
# Optional: Default query parameters
|
30
|
+
# config.query = {api_key: ENV['API_KEY']}
|
31
|
+
|
32
|
+
# Factory configuration
|
33
|
+
# config.factories.auto_discover = false # Default: true
|
34
|
+
# config.factories.paths += ["lib/factories"] # Adds to default paths
|
35
|
+
|
36
|
+
# Debug configuration
|
37
|
+
# Available in specs with debug: true (aliases: breakpoint, pry)
|
38
|
+
# Defaults to printing state overview (-> { puts inspect })
|
39
|
+
# Available context: expectation, variables, request, response,
|
40
|
+
# expected_status, expected_json
|
41
|
+
# config.on_debug { binding.pry }
|
42
|
+
|
43
|
+
# Test Framework Configuration
|
44
|
+
# Useful for database cleaners, test data setup, etc
|
45
|
+
# config.specs.before(:suite) { }
|
46
|
+
# config.specs.around { |example| example.run }
|
47
|
+
# config.specs.formatter = :documentation
|
37
48
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: spec_forge
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Bryan
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-02-
|
11
|
+
date: 2025-02-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|