Hanami actions are designed to be easy to test via a range of techniques.

The examples on this page use RSpec, the test framework installed when you generate a new Hanami app.

Testing actions

Actions are standalone objects with an interface that’s easy to test. You can simply instantiate an action as your object under test and exercise its functionality.

  1. # spec/app/actions/books/index_spec.rb
  2. RSpec.describe Bookshelf::Actions::Books::Index do
  3. subject(:action) do
  4. Bookshelf::Actions::Books::Index.new
  5. end
  6. it "returns a successful response with empty params" do
  7. response = action.call({})
  8. expect(response).to be_successful
  9. end
  10. end

In this example, action is an instance of Bookshelf::Actions::Books::Index. To make a request to the action, we can call it with an empty parameters hash. The return value is a serialized Rack response. In this test we’re asserting that the returned response is successful (status is in 2XX range).

Running Tests

To test your action, run bundle exec rspec with the path to your action’s test file:

  1. $ bundle exec rspec spec/actions/books/index_spec.rb

When you run the tests for a single action, Hanami will load only the smallest set of files required to run the test. The action’s dependencies and any related app code is loaded on demand, which makes it very fast to run and re-run individual tests as part of your development flow.

Your action tests will also be included when you run the whole test suite:

  1. $ bundle exec rspec

Providing params and headers

When testing an action, you can simulate the parameters and headers coming from a request by passing them as a hash.

Rack expects the headers to be uppercased, underscored strings prefixed by HTTP_ (like "HTTP_ACCEPT" => "application/json"), while your other request params can be regular keyword arguments.

The following test combines both params and headers.

  1. # spec/actions/books/show_spec.rb
  2. RSpec.describe Bookshelf::Actions::Books::Show do
  3. subject(:action) do
  4. Bookshelf::Actions::Books::Index.new
  5. end
  6. it "returns a successful JSON response with book id" do
  7. response = subject.call(id: "23", "HTTP_ACCEPT" => "application/json")
  8. expect(response).to be_successful
  9. expect(response.headers["Content-Type"]).to eq("application/json; charset=utf-8")
  10. expect(JSON.parse(response.body[0])).to eq("id" => "23")
  11. end
  12. end

Here’s the example action that would make this test pass.

  1. # app/actions/books/show.rb
  2. module Bookshelf
  3. module Actions
  4. module Users
  5. class Show < Action
  6. format :json
  7. def handle(request, response)
  8. response.body = {id: request.params[:id]}.to_json
  9. end
  10. end
  11. end
  12. end
  13. end

Mocking action dependencies

You may wish to provide test doubles (also known as “mock objects”) to your actions under test to control their environment or avoid unwanted side effects.

Since we directly instantiate our actions in our tests, we can provide these test doubles via dependency injection.

Let’s write the test for an action that creates a book such that it does not hit the database.

  1. # spec/actions/books/create_spec.rb
  2. RSpec.describe Bookshelf::Actions::Books::Create do
  3. subject(:action) do
  4. Bookshelf::Actions::Books::Create.new(user_repo: user_repo)
  5. end
  6. let(:user_repo) do
  7. instance_double(Bookshelf::UserRepo)
  8. end
  9. let(:book_params) do
  10. {title: "Hanami Guides"}
  11. end
  12. it "returns a successful response when valid book params are provided" do
  13. expect(user_repo).to receive(:create).with(book_params).and_return(book_params)
  14. response = action.call(book: book_params)
  15. expect(response).to be_successful
  16. expect(response.body[0]).to eq(book_params.to_json)
  17. end
  18. end

We’ve injected the user_repo dependency with an RSpec test double. This would replace the default "user_repo" component for the following action.

  1. # app/actions/books/create.rb
  2. module Bookshelf
  3. module Actions
  4. module Books
  5. class Create < Action
  6. include Deps["user_repo"]
  7. params do
  8. required(:book).hash do
  9. required(:title).value(:string)
  10. end
  11. end
  12. def handle(request, response)
  13. book = user_repo.create(request.params[:book])
  14. response.body = book.to_json
  15. end
  16. end
  17. end
  18. end
  19. end

Use test doubles only when the side effects are difficult to handle in a test environment. Remember to mock only your own interfaces and always use verified doubles.

Testing requests

Action tests are helpful for setting expectations on an action’s low-level behavior. However, for many actions, testing end-to-end behavior may be more useful.

For this, you can write request specs using [rack-test][rack-test], which comes included with your Hanami app.

  1. # spec/requests/root_spec.rb
  2. RSpec.describe "Root", type: :request do
  3. it "is successful" do
  4. # Find me in `config/routes.rb`
  5. get "/"
  6. expect(last_response).to be_successful
  7. expect(last_response.body).to eq("Hello from Hanami")
  8. end
  9. end

In many cases, you can rely on request tests and skip low-level action testing. Action tests only make sense when action logic becomes complex and you need to exercise many scenarios.

Avoid test doubles when writing request tests, since we want to verify that the whole stack is behaving as expected.