No release in over 3 years
rc-minitest-openapi turns Rails minitest integration tests into the source of truth for an OpenAPI 3.0 document. Tests declare each operation and its response schema, the live response is validated against that schema as the suite runs, and the openapi:generate rake task writes the document. An rswag-style block DSL and plain helper methods are both supported.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

 Project Readme

rc-minitest-openapi

Generate an OpenAPI 3.0 document from your Rails minitest API tests — an rswag-style workflow for teams that use minitest instead of RSpec.

Your integration tests declare each operation and the schema of its response. As the suite runs, every response body is validated against the schema it claims to return, and the openapi:generate rake task writes the assembled document. The tests are the single source of truth: if a response stops matching the contract, the suite goes red.

This repository contains three gems, mirroring rswag's split:

Gem Purpose
rc-minitest-openapi The test DSLs, response validation, and the openapi:generate rake task.
rc-minitest-openapi-api A mountable engine that serves the generated document.
rc-minitest-openapi-ui A mountable engine that renders Swagger UI.

Installation

# Gemfile
group :test do
  gem "rc-minitest-openapi"
end

# These two are only needed if you want to serve the doc / UI from the app:
gem "rc-minitest-openapi-api"
gem "rc-minitest-openapi-ui"

Configure

In test/test_helper.rb:

require "minitest/openapi"

Minitest::OpenAPI.configure do |config|
  config.output_path = "openapi/v1/openapi.json"   # where openapi:generate writes
  config.validate_responses = true                 # validate as the suite runs

  # The base document — everything not derived from tests. A Hash, or a path
  # to a JSON/YAML file. Test-recorded operations are merged into `paths`.
  config.base = "openapi/base.json"
end

The base document supplies info, servers, security schemes, and reusable components/schemas. Operations recorded by tests are merged into paths.

Writing tests

Two interchangeable DSLs sit on the same engine — use either, or both in the same suite. Both validate the live response against the declared schema and record the operation into the document.

Helper methods (classic minitest)

require "test_helper"

class MediaEntriesApiTest < ActionDispatch::IntegrationTest
  include Minitest::OpenAPI::DSL

  test "lists media entries" do
    openapi_get "/api/v1/media_entries",
      summary: "List media entries",
      operation_id: "listMediaEntries",
      tags: ["Media entries"],
      response: {
        status: 200,
        schema: {"$ref" => "#/components/schemas/MediaEntriesPage"}
      }
    assert_response :success
  end

  test "shows a media entry" do
    entry = media_entries(:published)
    openapi_get "/api/v1/media_entries/#{entry.id}",
      doc_path: "/api/v1/media_entries/{id}",
      summary: "Get a media entry",
      response: {status: 200, schema: {"$ref" => "#/components/schemas/MediaEntry"}}
    assert_response :success
  end
end

openapi_get / openapi_post / openapi_patch / openapi_put / openapi_delete perform the request, validate, record, and return the response so you can keep asserting. Pass doc_path: when the request URL is concrete but the documented path is templated.

Block DSL (minitest/spec)

require "test_helper"

class MediaEntriesApiTest < ActionDispatch::IntegrationTest
  extend Minitest::OpenAPI::Spec

  api_path "/api/v1/media_entries" do
    api_operation :get, summary: "List media entries" do
      api_response 200, schema: {"$ref" => "#/components/schemas/MediaEntriesPage"} do
        run_api_test!
      end
    end
  end

  api_path "/api/v1/media_entries/{id}" do
    api_operation :get, summary: "Get a media entry" do
      api_response 200, schema: {"$ref" => "#/components/schemas/MediaEntry"} do
        run_api_test! { {path: "/api/v1/media_entries/#{media_entries(:published).id}"} }
      end
    end
  end
end

run_api_test! defines the test. Its optional block runs in the test instance and returns request overrides — {path:, params:, headers:, body:} — so a templated path can be turned into a concrete URL from records the test sets up.

Generating the document

bin/rails openapi:generate                          # runs test/integration
bin/rails "openapi:generate[test/integration/api]"   # scope to specific paths

This runs your API tests with the document writer enabled and writes config.output_path. The output is byte-stable — paths, operations, and responses are emitted in a canonical order — so you can run it in CI and diff the result to catch a contract that changed without the spec being regenerated. Ordinary bin/rails test runs are unaffected — they still validate responses, they just don't write the file.

Serving the document and UI

# config/routes.rb
mount Minitest::OpenAPI::Api::Engine => "/api-docs"
mount Minitest::OpenAPI::UI::Engine  => "/api-docs/ui"
# config/initializers/minitest_openapi.rb
Minitest::OpenAPI::Api.configure { |c| c.document_path = "openapi/v1/openapi.json" }
Minitest::OpenAPI::UI.configure  { |c| c.openapi_url = "/api-docs" }

/api-docs serves the JSON; /api-docs/ui renders Swagger UI against it.

License

MIT — see LICENSE.txt.