Standalone testing of actions in rc1

Hi fellow rubyists,
I have somewhat simple hanami action, smth like:

class MyAction
  extend Dry::Initializer

  option :repo, default: -> { Repos::MyRepo }

  params do
    ....
  end
  def handle(req, res)
    res.body = repo.some_method(req.params[:id]).to_json
  end
end

Now, for testing, i am initialising this action,

  subject do
    described_class.new(repo: repo_double)
      .call(params)
  end

With that, i am getting an exception:

Failure/Error:
       described_class.new(repo: repo_double)
         .call(params)

     NoMethodError:
       undefined method `handled_exceptions' for nil:NilClass

             config.handled_exceptions.each do |exception_class, handler|
                   ^^^^^^^^^^^^^^^^^^^
     # /usr/local/bundle/gems/hanami-controller-2.0.0.rc1/lib/hanami/action.rb:437:in `exception_handler'
     # /usr/local/bundle/gems/hanami-controller-2.0.0.rc1/lib/hanami/action.rb:487:in `_handle_exception'
     # /usr/local/bundle/gems/hanami-controller-2.0.0.rc1/lib/hanami/action.rb:346:in `rescue in block in call'
     # /usr/local/bundle/gems/hanami-controller-2.0.0.rc1/lib/hanami/action.rb:325:in `block in call'
     # /usr/local/bundle/gems/hanami-controller-2.0.0.rc1/lib/hanami/action.rb:324:in `catch'
     # /usr/local/bundle/gems/hanami-controller-2.0.0.rc1/lib/hanami/action.rb:324:in `call'

Looking at the docu, standalone testing seems to require configuration object.


I am not sure what is exactly expected here. Hanami::Controller::Configuration (smth mentioned in the docs) does not seem to exist, and passing configuration: Hanami::Action.config as parameter to action initialisation does not seem to help either.

What I might be missing here to unit test the action ?

P.S. beta4 did not crash this way, I was simply initialising action with dependencies and invoking .call with hash of params.

@dimaboyko Here’s how to solve the problem:

# app/repositories/my_repo.rb
# frozen_string_literal: true

module Bookshelf
  module Repositories
    class MyRepo
    end
  end
end
# app/actions/books/index.rb
# frozen_string_literal: true

module Bookshelf
  module Actions
    module Books
      class Index < Bookshelf::Action
        # THIS IS A REPLACEMENT FOR EXPLICIT dry-initializer
        include Deps["repositories.my_repo"]

        def handle(*, response)
          response.body = self.class.name
        end
      end
    end
  end
end
# spec/actions/books/index_spec.rb
# frozen_string_literal: true

RSpec.describe Bookshelf::Actions::Books::Index do
  subject { described_class.new(my_repo: repo) }
  let(:params) { Hash[] }
  let(:repo) { double("repo") }

  it "works" do
    response = subject.call(params)
    expect(response).to be_successful
  end
end

Explanation: They keyword argument to pass is the “local name” of the import:

include Deps["repositories.my_repo"]

It requires my_repo: keyword argument

You can also be more explicit and use:

include Deps[my_repo: "repositories.my_repo"]

or

include Deps[foo: "repositories.my_repo"]

Which will require my_repo: and foo: keyword arguments, respectively.

Thanks @jodosha for your reply.
I am not using full hanami framework, only router and actions. Plus, my app structure is somewhat unusual ( i am not registering classes, used in actions as providers)
Therefore I believe I do not have all the inflectors available.
Is there a way to use my own DI for actions, and test them standalone ?

@dimaboyko Sorry I assumed so.

In this case, your setup with Dry::Initializer is correct.
In the testing phase you need to provide the dependencies:

  1. The action configuration
  2. The repo
subject { described_class.new(configuration: configuration, repo: repo)
let(:configuration) { Hanami::Action::Configuration.new }
let(:repo) { double("repo") }

If you have set a specific Hanami::Action::Configuration.new during your app setup, pass that instance, instead of a new one (like my let block).

Just a quick note here, Action’s default initialiser looks like this:

    # Returns a new action
    #
    # @since 2.0.0
    # @api public
    def initialize(config: self.class.config)
      @config = config
      freeze
    end

So you shouldn’t actually have to pass in a config; it’ll use the class-level config by default.

I think a problem here is that Hanami::Action in no way expects dry-initializer to be present, so if you want dry-initializer’s own initialize to run, you’d have to make sure you invoke it from within your own #initialize in your action base class, something like this:

class MyBaseActionClass < Hanami::Action
  def initialize(...)
    super()
    __dry_initializer_initialize__(...)
  end
end

Both Controller::Configuration (smth mentioned in docs) and what you @jodosha suggested (Hanami::Action::Configuration) seem to be not available. Are they coming from some other mixin, that i might be missing ?
I have hanami controller, router, validations, helpers in my gemfile, and utils is being fetched as dependency for controller.

Hanami::Action::Configuration
eval error: uninitialized constant Hanami::Action::Configuration

Hanami::Action::Configuration
              ^^^^^^^^^^^^^^^
(ruby) Hanami::Controller::Configuration
eval error: uninitialized constant Hanami::Controller::Configuration

Hanami::Controller::Configuration
                  ^^^^^^^^^^^^^^^

I think @timriley is correct, described_class.config is available. However, my test is still failing with

 NoMethodError:
       undefined method `handled_exceptions' for nil:NilClass
     
             config.handled_exceptions.each do |exception_class, handler|
                   ^^^^^^^^^^^^^^^^^^^
     # /usr/local/bundle/gems/hanami-controller-2.0.0.rc1/lib/hanami/action.rb:437:in `exception_handler'
. . . . . . . . ...
# --- Caused by: ---
     # NoMethodError:
     #   undefined method `accepted_mime_types' for nil:NilClass
     #   
     #             content_type: Mime.calculate_content_type_with_charset(config, request, config.accepted_mime_types),
     #                                                                                           ^^^^^^^^^^^^^^^^^^^^
     #   /usr/local/bundle/gems/hanami-controller-2.0.0.rc1/lib/hanami/action.rb:334:in `block in call'

It seems indeed like starting from rc1 dry initializer injection is not supported. And even overriding initialiser in BaseAction, like @timriley is suggesting, does not seem to solve the issue, config still appears to be nil when invoking action within unit test.