-
-
Notifications
You must be signed in to change notification settings - Fork 0
Migration Guide
SpecForge 1.0 is a complete architectural redesign. This guide walks you through migrating your existing tests to the new step-based workflow system.
What changed:
- File structure: Named specs -> Array of sequential steps
- Variable syntax:
variables.xxx->{{ xxx }} - Storage:
store_as:->store:attribute - Directory:
specs/->blueprints/ - Global config: Moved from YAML to
forge_helper.rb - Validation: New
shape:andschema:modes
What stayed the same:
- Faker integration (
faker.xxx) - FactoryBot integration (
factories.xxx) - RSpec matchers (
kind_of.,matcher.,be.) - OpenAPI generation workflow
- Core CLI commands
Rename your specs directory:
mv spec_forge/specs spec_forge/blueprintsUpdate any scripts or CI configs that reference the old path.
| 0.7 Command | 1.0 Command |
|---|---|
spec_forge new spec users |
spec_forge new blueprint users |
spec_forge run users:get_user |
spec_forge run blueprints/users.yml |
spec_forge run users:get_user:'GET /users' |
Use --tags for filtering |
This is the biggest conceptual change. In 0.7, files contained a hash of named specs. In 1.0, files contain an array of sequential steps.
# spec_forge/specs/users.yml
global:
variables:
admin_role: "admin"
create_user:
path: /users
method: POST
variables:
email: faker.internet.email
body:
email: variables.email
role: global.variables.admin_role
store_as: new_user
expectations:
- expect:
status: 201
json:
id: kind_of.integer
email: variables.email
get_user:
path: /users/{id}
query:
id: store.new_user.body.id
expectations:
- expect:
status: 200
json:
id: store.new_user.body.id# spec_forge/blueprints/users.yml
- name: "Create user"
request:
url: /users
http_verb: POST
json:
email: "{{ faker.internet.email }}"
role: "{{ admin_role }}"
expect:
- status: 201
json:
shape:
id: integer
email: string
store:
new_user_id: "{{ response.body.id }}"
new_user_email: "{{ response.body.email }}"
- name: "Get user"
request:
url: "/users/{{ new_user_id }}"
expect:
- status: 200
json:
content:
id: "{{ new_user_id }}"Key differences:
- Root is an array (
-), not a hash of named specs - Each step has a
name:attribute - Request config is nested under
request: - Variables use
{{ }}syntax -
store:captures specific values, not entire responses -
expect:is a direct array (notexpectations:withexpect:inside)
| 0.7 | 1.0 Primary | 1.0 Alias |
|---|---|---|
path: |
url: |
path: works |
method: |
http_verb: |
method: works |
body: |
json: |
body: for non-JSON |
create_user:
path: /users
method: POST
body:
name: "Test User"- name: "Create user"
request:
url: /users
http_verb: POST
json:
name: "Test User"Note: The aliases (path:, method:) still work, but examples and docs use the primary names.
Before (0.7):
create_user:
variables:
email: faker.internet.email
body:
email: variables.emailAfter (1.0):
- store:
email: "{{ faker.internet.email }}"
- name: "Create user"
request:
json:
email: "{{ email }}"Or inline with the request:
- name: "Create user"
request:
json:
email: "{{ faker.internet.email }}"Global variables moved from YAML to forge_helper.rb:
Before (0.7):
# In your spec file
global:
variables:
admin_role: "admin"
api_version: "v1"
create_user:
body:
role: global.variables.admin_roleAfter (1.0):
# spec_forge/forge_helper.rb
SpecForge.configure do |config|
config.base_url = "http://localhost:3000"
config.global_variables = {
admin_role: "admin",
api_version: "v1"
}
end# In your blueprint
- name: "Create user"
request:
json:
role: "{{ admin_role }}"Why this changed: Global variables are now truly global - available across all blueprints, not just one file.
The store_as: directive is replaced by the store: attribute with explicit field extraction.
create_user:
path: /users
method: POST
body:
name: "Test User"
store_as: new_user
expectations:
- expect:
status: 201
get_user:
path: /users/{id}
query:
id: store.new_user.body.id- name: "Create user"
request:
url: /users
http_verb: POST
json:
name: "Test User"
expect:
- status: 201
store:
user_id: "{{ response.body.id }}"
- name: "Get user"
request:
url: "/users/{{ user_id }}"Key differences:
-
store:explicitly names what to capture - Reference stored values directly (
{{ user_id }}) not viastore.xxx.body.id -
response.bodyandresponse.headersavailable in store expressions
| 0.7 | 1.0 |
|---|---|
store.new_user.body.id |
{{ user_id }} (after storing) |
store.new_user.headers.Location |
{{ location }} (after storing) |
store.new_user.status |
Not typically needed |
To store multiple values:
store:
user_id: "{{ response.body.id }}"
user_email: "{{ response.body.email }}"
location: "{{ response.headers.location }}"The structure simplified: expectations: with nested expect: became just expect: as an array.
get_user:
path: /users/1
expectations:
- expect:
status: 200
json:
id: kind_of.integer
name: kind_of.string- name: "Get user"
request:
url: /users/1
expect:
- status: 200
json:
shape:
id: integer
name: string1.0 introduces explicit validation modes:
shape: - Type validation (most common)
json:
shape:
id: integer
name: string
email: string
tags: [string] # Array of strings
metadata: ?hash # Nullablecontent: - Exact value matching
json:
content:
status: "active"
role: "{{ admin_role }}"schema: - Advanced control (rare)
json:
schema:
type: array
structure:
- integer
- stringImportant: shape: and schema: only accept type names - they don't support {{ }} interpolation or matchers. If you need matcher-based validation, use content::
# For type checking - use shape with type names
json:
shape:
id: integer
name: string
# For matcher-based validation - use content
json:
content:
id: "{{ kind_of.integer }}"
tags:
matcher.include: "featured"1.0 introduces type flags for handling fields that may be missing or null -- something 0.7 had no built-in way to express.
| Flag | Meaning | Example |
|---|---|---|
? |
Nullable (value can be null) | ?string |
* |
Optional (key can be missing) | *string |
Flags combine in any order:
json:
shape:
id: integer # Required, non-null
deleted_at: ?string # Required, can be null
nickname: *string # Optional, non-null if present
bio: *?string # Optional, can be nullBehavior matrix:
| Syntax | Key Missing | Value Null | Value Present |
|---|---|---|---|
string |
[x] fail | [x] fail | [ok] pass |
?string |
[x] fail | [ok] pass | [ok] pass |
*string |
[ok] pass | [x] fail | [ok] pass |
*?string |
[ok] pass | [ok] pass | [ok] pass |
See Validating Responses for complete coverage.
0.7 supported global headers in configuration. 1.0 removed this in favor of explicit inheritance.
Before (0.7):
# forge_helper.rb
SpecForge.configure do |config|
config.headers = {
"Authorization" => "Bearer #{ENV['API_TOKEN']}"
}
endAfter (1.0):
# Use nesting with shared: wrapper
- name: "Authenticated requests"
shared:
request:
headers:
Authorization: "Bearer {{ api_token }}"
steps:
- name: "Get profile"
request:
url: /me
- name: "Update profile"
request:
url: /me
http_verb: PUTWhy this changed: Explicit headers in blueprints make tests self-documenting. You can see exactly what headers are sent by reading the YAML.
Use steps: with the shared: wrapper to group steps and share configuration.
There wasn't a direct equivalent - you'd repeat headers on each spec.
- name: "User CRUD"
shared:
request:
headers:
Authorization: "Bearer {{ token }}"
steps:
- name: "Create"
request:
url: /users
http_verb: POST
json:
name: "Test"
store:
user_id: "{{ response.body.id }}"
- name: "Read"
request:
url: "/users/{{ user_id }}"
- name: "Delete"
request:
url: "/users/{{ user_id }}"
http_verb: DELETEAll nested steps inherit the Authorization header.
Note: shared: also works for hook: configuration, not just request:. See Callbacks for details.
# forge_helper.rb
SpecForge.configure do |config|
config.register_callback(:seed_db) do |context|
# Setup code
end
endBefore (0.7):
global:
callbacks:
- before_file: seed_db
after_file: cleanup_dbAfter (1.0):
- hook:
before_blueprint: seed_db
after_blueprint: cleanup_db| 0.7 | 1.0 |
|---|---|
before_file |
before_blueprint |
after_file |
after_blueprint |
before |
before_step |
after |
after_step |
New in 1.0:
-
before_forge/after_forge- Run once for entire test suite
Tags let you filter which steps run:
- name: "Health check"
tags: [critical, public]
request:
url: /health
- name: "Admin dashboard access"
tags: [admin, authenticated]
request:
url: /admin/dashboardspec_forge run --tags critical
spec_forge run --skip-tags admin
spec_forge run --tags public,authenticated --skip-tags admin# spec_forge/specs/posts.yml
global:
variables:
default_status: "draft"
callbacks:
- before_file: seed_database
create_post:
path: /posts
method: POST
variables:
title: faker.lorem.sentence
body:
title: variables.title
status: global.variables.default_status
store_as: new_post
expectations:
- expect:
status: 201
json:
id: kind_of.integer
title: variables.title
get_post:
path: /posts/{id}
query:
id: store.new_post.body.id
expectations:
- expect:
status: 200
json:
id: store.new_post.body.id
status: global.variables.default_status
update_post:
path: /posts/{id}
method: PUT
query:
id: store.new_post.body.id
body:
status: "published"
expectations:
- expect:
status: 200
json:
status: "published"# spec_forge/blueprints/posts.yml
- hook:
before_blueprint: seed_database
- name: "Create post"
request:
url: /posts
http_verb: POST
json:
title: "{{ faker.lorem.sentence }}"
status: "{{ default_status }}"
expect:
- status: 201
json:
shape:
id: integer
title: string
store:
post_id: "{{ response.body.id }}"
- name: "Get post"
request:
url: "/posts/{{ post_id }}"
expect:
- status: 200
json:
content:
id: "{{ post_id }}"
status: "{{ default_status }}"
- name: "Update post"
request:
url: "/posts/{{ post_id }}"
http_verb: PUT
json:
status: "published"
expect:
- status: 200
json:
content:
status: "published"With the global variable in forge_helper.rb:
SpecForge.configure do |config|
config.base_url = "http://localhost:3000"
config.global_variables = {
default_status: "draft"
}
end1. Forgetting request: wrapper
# Wrong
- name: "Get user"
url: /users/1
# Right
- name: "Get user"
request:
url: /users/12. Missing {{ }} in string interpolation
# Wrong
email: faker.internet.email
# Right
email: "{{ faker.internet.email }}"3. Using old store syntax
# Wrong
url: "/users/{{ store.new_user.body.id }}"
# Right (after storing as user_id)
url: "/users/{{ user_id }}"4. Combining actions with steps:
You cannot combine action attributes (request, expect, call, debug, store) with steps:. A step either executes an action OR organizes substeps, not both.
# Wrong - can't mix action with steps
- name: "Auth requests"
request: # This is an action
headers:
Authorization: "Bearer {{ token }}"
steps: # Can't have both!
- name: "Get profile"
request:
url: /me
# Right - use shared: wrapper for inheritance
- name: "Auth requests"
shared:
request:
headers:
Authorization: "Bearer {{ token }}"
steps:
- name: "Get profile"
request:
url: /me # Inherits auth headerWhy this matters: The shared: wrapper makes inheritance explicit — anything in shared: cascades down to nested steps. Without it, there's ambiguity about what applies where.
- Writing Tests - Complete 1.0 syntax reference
- Configuration - Setup and global variables
- GitHub Issues - Report migration problems