| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [View Raw Code] [Original HTTPS Page] |
ActionFigure actions return plain hashes, making them straightforward to test without controller setup or request scaffolding. You call the action directly, receive a result, and assert against it.
Both Minitest and RSpec helpers are provided. They wrap status checks in expressive, intention-revealing assertions so your tests read clearly.
Require the helper and include the module in your test class:
require "action_figure/testing/minitest"
class Users::CreateActionTest < Minitest::Test
include ActionFigure::Testing::Minitest
end| Assertion | Expected status |
|---|---|
| assert_Ok(result) | :ok |
| assert_Created(result) | :created |
| assert_Accepted(result) | :accepted |
| assert_NoContent(result) | :no_content |
| assert_UnprocessableContent(result) | :unprocessable_content |
| assert_NotFound(result) | :not_found |
| assert_Forbidden(result) | :forbidden |
| assert_Conflict(result) | :conflict |
| assert_PaymentRequired(result) | :payment_required |
| assert_Gone(result) | :gone |
| assert_Locked(result) | :locked |
| assert_UnavailableForLegalReasons(result) | :unavailable_for_legal_reasons |
Each status assertion has a negated counterpart — refute_Ok, refute_Created, … — that passes when the status is anything other than the named one. Statuses added with ActionFigure.register_error get matching assert_*/refute_* helpers automatically, whether registered before or after this adapter loads.
These helpers compare only result[:status] against the Rack-style symbol Rails uses in render — they do not assert on [:json] keys, payloads, or error message text. Combine them with assertions on result[:json] (or matchers on the body your formatter produces) whenever shape matters.
All assertions accept an optional second argument for a custom failure message:
assert_Ok(result, "expected the user to be created successfully")When a status assertion fails, the default message shows the expected and actual status:
Expected result status to be :ok, but got :unprocessable_content
Use assert_action_json to match a (possibly nested) subset of result[:json] — the Minitest counterpart to RSpec's have_action_json. Nested Hashes match as subsets, and Regexp values match against strings:
assert_action_json(result, status: "success")
assert_action_json(result, status: "success", data: { name: "Jane" })
assert_action_json(result, data: { email: /@example\.com\z/ })refute_action_json passes when the fragment does not match. Both fail with a clear message when given a non-result value or a hash missing the :json key.
Require the helper in your spec support file. No include is needed -- the matchers are registered globally:
# spec/spec_helper.rb
require "action_figure/testing/rspec"Load order: require this library after RSpec Core and expectations load (usual practice: append it toward the bottom of spec/spec_helper.rb, after any require "rails_helper" / RSpec.configure boilerplate from your app). ActionFigure pulls in rspec/matchers; minimalist scripts without the full rspec CLI shim must require "rspec/expectations" (and typically require "rspec/core") before this file.
| Matcher | Expected status |
|---|---|
| be_Ok | :ok |
| be_Created | :created |
| be_Accepted | :accepted |
| be_NoContent | :no_content |
| be_UnprocessableContent | :unprocessable_content |
| be_NotFound | :not_found |
| be_Forbidden | :forbidden |
| be_Conflict | :conflict |
| be_PaymentRequired | :payment_required |
| be_Gone | :gone |
| be_Locked | :locked |
| be_UnavailableForLegalReasons | :unavailable_for_legal_reasons |
| have_action_json | result[:json] matches a_hash_including(fragment) |
| accept_params(params) | action class's contract accepts params |
| reject_params(params) | action class's contract rejects params (chain .with_error_on(:field)) |
Like the Minitest helpers, each be_* matcher compares only result[:status] — [:json] is ignored unless you assert on it separately. Use have_action_json when you want a focused assertion against the json body (compose with a_hash_including for nested subsets):
expect(result).to be_Ok
expect(result).to have_action_json(status: "success")
expect(result).to have_action_json(
status: "success",
data: a_hash_including(name: "Jane")
)Matchers support negation:
expect(result).to be_Ok
expect(result).not_to be_ForbiddenFailure messages mirror the Minitest style:
expected result status to be :ok, but got :unprocessable_content
The examples below use Minitest, but the same patterns apply to RSpec with the corresponding matchers.
The examples below use the JSend formatter (ActionFigure[:jsend]) for consistency. The structure of result[:json] depends on your chosen formatter — see Response Formatters for the shape each format produces.
Call your class and assert both the status and the returned data:
class Users::CreateActionTest < Minitest::Test
include ActionFigure::Testing::Minitest
def test_creates_a_user
result = Users::CreateAction.create(params: { email: "jane@example.com", name: "Jane" })
assert_Ok(result)
assert_equal "jane@example.com", result[:json][:data][:email]
assert_equal "Jane", result[:json][:data][:name]
end
endWhen testing validation failures, assert both the status and the error message content. Testing only the status is insufficient -- it does not prove the correct validation failed.
class Users::CreateActionTest < Minitest::Test
include ActionFigure::Testing::Minitest
def test_rejects_missing_email
result = Users::CreateAction.create(params: { name: "Jane" })
assert_UnprocessableContent(result)
assert_includes result[:json][:data][:email], "is missing"
end
endActions often receive context such as current_user: as keyword arguments alongside params:. Pass them directly in the test:
class Posts::CreateActionTest < Minitest::Test
include ActionFigure::Testing::Minitest
def test_creates_a_post_for_the_current_user
user = users(:jane)
result = Posts::CreateAction.create(params: { title: "Hello", body: "World" }, current_user: user)
assert_Created(result)
assert_equal user.id, result[:json][:data][:author_id]
end
endCall the action using its discovered method name:
class Products::SearchActionTest < Minitest::Test
include ActionFigure::Testing::Minitest
# class SearchAction
# include ActionFigure[:jsend]
#
# params_schema do
# required(:query).filled(:string)
# end
#
# def search(params:, **)
# products = Product.where("name ILIKE ?", "%#{params[:query]}%")
# Ok(resource: products)
# end
# end
def test_finds_matching_products
result = SearchAction.search(params: { query: "keyboard" })
assert_Ok(result)
assert result[:json][:data].any?, "expected at least one matching product"
end
endActions that perform side effects without returning data use NoContent():
class Sessions::DestroyActionTest < Minitest::Test
include ActionFigure::Testing::Minitest
# class Sessions::DestroyAction
# include ActionFigure[:jsend]
#
# def destroy(session:)
# session.destroy!
# NoContent()
# end
# end
def test_destroys_the_session
session = sessions(:active)
result = Sessions::DestroyAction.destroy(session: session)
assert_NoContent(result)
end
endEvery action that defines a params_schema exposes the underlying validation contract via .contract. This returns a Dry::Validation::Contract instance that you can call directly -- useful for validating input without executing the action.
contract = Users::CreateAction.contract
result = contract.call(email: "jane@example.com", name: "Jane")
result.success? # => true
result.to_h # => { email: "jane@example.com", name: "Jane" }When validation fails, inspect the errors:
result = Users::CreateAction.contract.call(email: "", name: "Jane")
result.failure? # => true
result.errors.to_h # => { email: ["must be filled"] }This runs both the schema and any rules defined on the action -- the same validation pipeline that the class-level trigger uses, without the side effects.
Actions that do not define a params_schema return nil from .contract.
The contract exposes the schema and rules for introspection:
contract = Users::CreateAction.contract
contract.schema # => the Dry::Schema::Params instance
contract.schema.key_map.map(&:name) # => ["email", "name"]
contract.rules # => array of Dry::Validation::Rule objects
contract.rules.map(&:keys) # => [[:email]]This is useful for building documentation generators, admin panels, or debugging which validations an action enforces.
class Users::CreateActionTest < Minitest::Test
def test_email_is_required
result = Users::CreateAction.contract.call(name: "Jane")
assert result.failure?
assert_includes result.errors.to_h[:email], "is missing"
end
endThe testing adapters wrap the .contract.call boilerplate above in intention-revealing helpers. They are formatter-agnostic — they exercise the validation pipeline directly, so the same assertions work regardless of which formatter the action includes.
Minitest — the subject is the action class:
class Users::CreateActionTest < Minitest::Test
include ActionFigure::Testing::Minitest
def test_accepts_valid_params
assert_valid_params(Users::CreateAction, { email: "jane@example.com", name: "Jane" })
end
def test_requires_email
# passes when the contract rejects the params at all
assert_invalid_params(Users::CreateAction, { name: "Jane" })
# scope to a field: passes only when :email is among the errors
assert_invalid_params(Users::CreateAction, { name: "Jane" }, on: :email)
end
endRSpec — the subject is the action class, not a result hash:
RSpec.describe Users::CreateAction do
it "accepts valid params" do
expect(Users::CreateAction).to accept_params(email: "jane@example.com", name: "Jane")
end
it "requires email" do
expect(Users::CreateAction).to reject_params(name: "Jane")
expect(Users::CreateAction).to reject_params(name: "Jane").with_error_on(:email)
end
endBoth adapters raise a clear ArgumentError when the action class declares no params_schema (and therefore has no contract to validate against).
Validation errors vs. error bodies. These helpers are the right tool for asserting validation behavior. There is no formatter-agnostic helper for non-validation error bodies (e.g. a NotFound/Conflict you return with a custom errors: payload) — each formatter stores those differently and the result hash carries no formatter identity. Assert those with a format-specific assert_action_json / have_action_json.
| Back | FazBrowse Home | New Git URL |