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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 658fd0830ce9179c1fe7ea880e2ed8196ccbc47bc8b244f6fc40c458ca900cca
4
- data.tar.gz: 3658379f52f23ffa362ded1c50b28e052911b6602ac3a3bed02b1fd9c8ee334c
3
+ metadata.gz: 2f51faa712bafdcb48e3940273ec494eb3f39e43c0d122ff0a6f97e425026438
4
+ data.tar.gz: fe5cf7dff0a44cf5cfc466189fb38c3ed66da21637e18ebcdb44e31f9f6a8b73
5
5
  SHA512:
6
- metadata.gz: ebcdfd4e02d965049cabd1a18b7897665800dc8081149aa4554433e7083296f0121d76f4ec474b527fdd8df0da1c71170c9c10c5cff1a108f8704b7f14e7104f
7
- data.tar.gz: ad3b84d27b9e7ab62dc902ad2a5d41a77670286aaba9a635ccebf5735061fa725e8d46daf62fd75562a35688d62b3dc595a597b9e38ec965f5273fdc27a2aabb
6
+ metadata.gz: be068681e3c2ce8b2f62fa21ae03281d40b34c19e190acce4f90796dd526197b6ae54cc6edaa04d6715e347a67e2c193e2b99b0e16e45b47b3183915001790f3
7
+ data.tar.gz: 644f2c1a49d3ed45a201cb53d731176a418c134679f324ef96f331427481643b2744bd6c27c0f326df32c1db751c412137ad209a3f0b0ce3add9cc68eb6a660e
data/.standard.yml CHANGED
@@ -1,3 +1,7 @@
1
1
  # For available configuration options, see:
2
2
  # https://github.com/standardrb/standard
3
3
  ruby_version: 3.3
4
+ ignore:
5
+ - ".direnv/**/*"
6
+ - "vendor/**/*"
7
+ - "spec/integration/**/*"
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.4.0...HEAD
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. **Accessible API Testing**: Non-developers can write and maintain tests without Ruby knowledge. The YAML syntax reads like documentation.
27
- 2. **Living Documentation**: Tests serve as clear, maintainable documentation of your API's expected behavior.
28
- 3. **Power Without Complexity**: Get the benefits of Ruby-based tests (dynamic data, factories, matchers) without writing Ruby code.
29
- 4. **Quick Setup**: Start testing APIs without configuring HTTP clients or writing boilerplate code.
30
- 5. **Gradual Adoption**: Use alongside your existing test suite. Keep complex tests in RSpec while making simple API tests more accessible.
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
- Or with bundle:
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
- This creates `spec_forge/specs/users.yml`. Here's a basic example:
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
- Since `run` is the default command, you can just use:
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
- ### Path Parameters
83
+ For comprehensive documentation, visit the [SpecForge Wiki](https://github.com/itsthedevman/spec_forge/wiki) which includes:
434
84
 
435
- Use placeholders for dynamic path parameters:
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
- ```yaml
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
- ## Factory Support
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
- Becomes this RSpec test:
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
- ```ruby
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
- 1. Fork it
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
- ## Changelog
716
-
717
- See [CHANGELOG.md](CHANGELOG.md) for a list of changes.
116
+ ## Looking for a Software Engineer?
718
117
 
719
- ## Credits
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
- - Author: Bryan "itsthedevman"
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
- BUILD_STRATEGIES = %w[
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
- # Determine build strat
40
- build_strategy = attributes[:build_strategy].resolve_value
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 == "stubbed"
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
- attributes = attributes[:attributes].resolve_value
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 uses_positional_arguments?(faker_method)
29
- faker_method.call(*arguments[:positional].resolve)
30
- elsif uses_keyword_arguments?(faker_method)
31
- faker_method.call(**arguments[:keyword].resolve)
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 uses_positional_arguments?(matcher_method)
48
- positional = arguments[:positional].resolve.each do |value|
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 uses_keyword_arguments?(matcher_method)
54
- matcher_method.call(**arguments[:keyword].resolve.deep_stringify_keys)
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
@@ -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(&:resolve)
196
+ value.map(&resolvable_proc)
195
197
  when HashLike
196
- value.transform_values(&:resolve)
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
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ return if defined?(SPEC_FORGE_INTERNAL_TESTING)
4
+
3
5
  module RSpec
4
6
  module Core
5
7
  module Notifications
@@ -8,7 +8,7 @@ module SpecForge
8
8
  type: Integer
9
9
  },
10
10
  json: {
11
- type: Hash,
11
+ type: [Hash, Array],
12
12
  default: {}
13
13
  }
14
14
  }.freeze
@@ -12,6 +12,11 @@ module SpecForge
12
12
  type: String,
13
13
  aliases: %i[strategy],
14
14
  default: "create"
15
+ },
16
+ size: {
17
+ type: Integer,
18
+ aliases: %i[count],
19
+ default: 0
15
20
  }
16
21
  }.freeze
17
22
  end
@@ -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.deep_stringify_keys }
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 constraints.json.size > 0
54
- expect(response.body).to be_kind_of(Hash)
55
- expect(response.body).to include(expected_json)
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: normalize_hash(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 normalize_hash(hash)
27
- hash =
28
- hash.transform_values do |attribute|
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
- Attribute.from(hash)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SpecForge
4
- VERSION = "0.4.0"
4
+ VERSION = "0.5.0"
5
5
  end
@@ -1,37 +1,48 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- ## Using Rails? Uncomment to load your app
4
- # ENV["RAILS_ENV"] ||= "test"
5
- # require_relative "../config/environment"
3
+ ##########################################
4
+ # Framework Integration
5
+ ##########################################
6
6
 
7
- ## Not using Rails? Load anything you need here
8
- # Dir[SpecForge.root.join("lib", "my_api", "models", "**/*.rb")].sort.each { |path| require path }
7
+ # Rails Integration
8
+ # require_relative "../config/environment"
9
9
 
10
- ## Using RSpec? Uncomment to use your existing configurations
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
- ## Base URL prefix for all API requests. All test paths will be appended to this URL
21
+ # Base configuration
15
22
  config.base_url = "http://localhost:3000"
16
23
 
17
- ## Default request headers - commonly used for authentication and content negotiation
18
- api_token = ENV.fetch("API_TOKEN", "")
24
+ # Default request headers
19
25
  config.headers = {
20
- "Authorization" => "Bearer #{api_token}"
26
+ "Authorization" => "Bearer #{ENV.fetch("API_TOKEN", "")}"
21
27
  }
22
28
 
23
- ## Default query parameters - useful for API keys or additional request context
24
- # config.query = {api_token:}
25
-
26
- ## Factory configuration options
27
- ##
28
- ## Enable/disable automatic factory discovery. When enabled, SpecForge will automatically
29
- ## load factories from FactoryBot's default paths. Note: Factories defined in
30
- ## "spec_forge/factories" are always loaded regardless of this setting.
31
- # config.factories.auto_discover = false # Default: true
32
-
33
- ##
34
- ## Additional paths, relative to the project folder, for discovering FactoryBot factories
35
- ## By default, FactoryBot looks in "spec/factories" and "test/factories"
36
- # config.factories.paths += ["custom/factories/path"]
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.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-23 00:00:00.000000000 Z
11
+ date: 2025-02-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport