Generate OpenAPI 3.1 documentation from your Minitest integration tests. No DSL, no magic - just one helper method.
Add to your Gemfile:
gem "openapi_minitest"Then run:
bundle install# config/initializers/openapi_minitest.rb (Rails)
# or test/support/openapi.rb
OpenapiMinitest.configure do |config|
config.title = "My API"
config.version = "1.0.0"
config.description = "API documentation"
config.output_path = "doc/openapi.yml" # YAML by default
config.servers = ["https://api.example.com"]
endOpenapiMinitest.define_schema :User, {
type: :object,
properties: {
id: { type: :integer },
email: { type: :string, format: :email },
name: { type: :string }
},
required: %w[id email]
}
OpenapiMinitest.define_schema :UserList, {
type: :object,
properties: {
data: { type: :array, items: { "$ref" => "#/components/schemas/User" } },
meta: {
type: :object,
properties: {
total: { type: :integer },
page: { type: :integer }
}
}
}
}
OpenapiMinitest.define_schema :Error, {
type: :object,
properties: {
error: { type: :string },
code: { type: :integer }
}
}Write normal Minitest tests. Call document_response after each request you want documented:
class UsersApiTest < ActionDispatch::IntegrationTest
def test_returns_users
create(:user, name: "John")
create(:user, name: "Jane")
get "/api/users", headers: auth_headers
assert_response 200
document_response schema: :UserList, description: "Returns all users"
body = JSON.parse(response.body)
assert_equal 2, body["data"].size
end
def test_filters_users_by_name
create(:user, name: "John")
create(:user, name: "Jane")
get "/api/users", params: { q: "John" }, headers: auth_headers
assert_response 200
document_response schema: :UserList, description: "Filters users by name"
body = JSON.parse(response.body)
assert_equal 1, body["data"].size
end
def test_returns_single_user
user = create(:user, name: "John")
get "/api/users/#{user.id}", headers: auth_headers
assert_response 200
document_response schema: :User, description: "User found", tags: ["Users"]
end
def test_user_not_found
get "/api/users/999999", headers: auth_headers
assert_response 404
document_response schema: :Error, description: "User not found"
end
def test_creates_user
post "/api/users",
params: { user: { name: "New User", email: "new@example.com" } },
headers: auth_headers,
as: :json
assert_response 201
document_response schema: :User, description: "User created", tags: ["Users"]
end
def test_create_user_validation_error
post "/api/users",
params: { user: { name: "" } },
headers: auth_headers,
as: :json
assert_response 422
document_response schema: :Error, description: "Validation failed"
end
private
def auth_headers
{ "Authorization" => "Bearer #{generate_token}" }
end
end# Run tests with documentation generation
OPENAPI_GENERATE=true rails test test/integration/
# Or use the rake task
rails openapi:generateRun the install generator to add an API docs page powered by Scalar:
rails generate openapi_minitest:installThis creates:
app/controllers/api_docs_controller.rb— serves the docs UI and the OpenAPI spec fileapp/views/api_docs/index.html.erb— Scalar API reference page (titled with your Rails app name)- Routes:
GET /api-docsandGET /openapi.yml
Visit /api-docs in your browser to explore your API documentation interactively.
OpenapiMinitest.configure do |config|
config.title = "My API" # API title
config.version = "1.0.0" # API version
config.description = "Description" # API description
config.output_path = "doc/openapi.yml" # Output file path (YAML by default)
config.servers = [ # Server URLs
"https://api.example.com",
{ url: "https://staging.example.com", description: "Staging" }
]
config.security_schemes = { # Security schemes
bearer: {
type: :http,
scheme: :bearer
}
}
config.validate_schema = true # Validate responses against schemas
config.strict_validation = false # Fail if response contains undocumented fields
endCall after making a request to record it for documentation:
document_response(
schema: :SchemaName, # Schema reference (Symbol) or inline schema (Hash)
summary: "Operation summary", # Defaults to test name
description: "Response desc", # Description of this response
tags: ["Tag1", "Tag2"], # Tags for grouping
operation_id: "getUsers", # Unique operation ID
deprecated: false, # Mark endpoint as deprecated
strict: nil # Override strict validation (true/false/nil for global config)
)# Reference to another schema
OpenapiMinitest.define_schema :UserList, {
type: :object,
properties: {
data: { type: :array, items: { "$ref" => "#/components/schemas/User" } }
}
}
# Inline schema in tests
document_response schema: {
type: :object,
properties: {
status: { type: :string }
}
}
# Nullable fields (OpenAPI 3.1 syntax - use type array instead of nullable: true)
OpenapiMinitest.define_schema :Article, {
type: :object,
properties: {
id: { type: :integer },
title: { type: :string },
subtitle: { type: [:string, :null] }, # nullable string
published_at: { type: [:string, :null], format: :datetime }
}
}- No DSL - Just one helper method, write normal Minitest tests
- Schema validation - Optionally validate responses against schemas during tests
- Strict validation - Fail tests when responses contain undocumented fields
- Auto-detection - Automatically extracts path parameters, query params
- Security handling - Authorization headers automatically use configured security schemes
- Multiple examples - Each test becomes an example in the docs
- Request bodies - Captures POST/PUT/PATCH request bodies as examples
Enable strict validation to catch undocumented fields in API responses. This helps ensure your documentation stays in sync with your implementation.
When strict mode is enabled, the test will fail if the response contains any fields not defined in the schema:
Response does not match schema:
The property '#/' contains additional properties ["unexpected_field"] outside of the schema when none are allowed
OpenapiMinitest.configure do |config|
config.strict_validation = true # All schemas strictly validated
end# Force strict validation for this endpoint
document_response schema: :User, strict: true
# Disable strict validation for this endpoint (e.g., third-party responses)
document_response schema: :ExternalData, strict: false
# Use global config setting (default behavior)
document_response schema: :UserStrict validation works by automatically injecting additionalProperties: false into all object schemas during validation. This includes nested objects and objects within arrays. If a schema already defines additionalProperties, that value is preserved.
- You write normal integration tests
- Call
document_responseafter requests you want documented - The gem captures:
- Request method, path, parameters, headers
- Response status, body
- Schema reference or definition
- After tests run, generates OpenAPI 3.1 YAML
The gem automatically detects numeric IDs in paths and converts them:
/api/users/123 -> /api/users/{user_id}
/api/users/123/posts/456 -> /api/users/{user_id}/posts/{post_id}
When you configure security_schemes and your tests include an Authorization header, the gem automatically adds the security property to those operations:
OpenapiMinitest.configure do |config|
config.security_schemes = {
bearer: {
type: :http,
scheme: :bearer
}
}
endOperations with Authorization headers will be generated with:
security:
- bearer: []This gem generates OpenAPI 3.1.0 which supports type arrays for nullable fields:
type:
- string
- 'null'Some validators may not fully support OpenAPI 3.1.0 yet. If you encounter validation errors about type arrays, ensure your validator supports OpenAPI 3.1.0.
Include the DSL module manually:
class MyApiTest < Minitest::Test
include OpenapiMinitest::DSL
# ... your tests
end
# Generate documentation after tests
Minitest.after_run do
if ENV["OPENAPI_GENERATE"]
OpenapiMinitest::OpenAPI::Generator.new.write
end
endAfter checking out the repo, run bin/setup to install dependencies. Then, run rake test to run the tests.
Bug reports and pull requests are welcome on GitHub at https://github.com/k0va1/openapi_minitest.
MIT