Using Dry::Container with test mocks

I have this pattern come up over and over again:

class MyAdapter
  include Container[:my_client]

  def initialize(credentials, **deps)
    @credentials = credentials
    super(**deps)
  end
end

class DoSomething
  include Container[:my_adapter]

  def call(account)
    adapter = my_adapter.new(account.credentials)
    # ...
  end
end

module Container
  register(:my_client) { MyClient }
  register(:my_adapter) { MyAdapter }
end

RSpec.describe DoSomething
  let(:response) { { some: :json } }
  let(:client) { instance_double(MyClient, request: response) }
  let(:client_class) { class_double(MyClient, new: client) }

  let(:adapter_class) { class_double(MyAdapter, new: MyAdaper.new(my_client: client_class)) }
  # -- or --
  before { Container.stub(:my_client, client_class) }

  subject(:something) { DoSomething.new(my_adapter: adapter).call(account) }

  specify {
    expect(adapter_class).to have_received(:new).with(credentials) 
  }
  
  specify {
    expect(client).to have_received(:request).with(some_data) 
  }
end

I have classes that need to be initialized with some data besides their dependencies (in this case, some API credentials), and I have to set up those tedious class+instance mocks.

I noticed dry-rails’ autoloader uses dry-system to somehow auto-“new” things things it discovers to register (eg, loading “app/whatever/my_command.rb” causes AppContainer["my_command"] to return an instance of MyCommand rather than the class itself. Is there a way to leverage that, but have control over the arguments that get passed to initialize?

Or is the expected pattern to just never have any objects that have to be initialized with non-dependencies? (Note this is difficult for things like adapters, which tend to have several methods that take independent args like the params to the API call as well as some data that’s always the same, like credentials).

I’ve also tried thing like defining #new as an instance method, so I can call it as many times as I want on an object, which is useful for tests, but feels gross otherwise.

def new(credentials, **deps)
  self.class.new(credentials, **deps)
end

Another approach would be to separate the “dependencies” from the rest of the args, and make that a separate method/step. This feels decidedly non-ruby-like, though.

class MyAdapter
  include Container[:my_client]

  def authenticate!(credentials)
    @credentials = credentials
  end

  def make_request(args)
    raise NotAuthenticated unless @credentials
    # do the work
  end
end

adapter = MyAdapter.new(dependencies)
adapter.authenticate!(account.credentials)
adapter.make_request(...)

I worked around this issue with a custom bootable. Checkout: Alba and Dependency Injections · Issue #105 · okuramasafumi/alba · GitHub

You could create an adapter bootable and register your adapters class constants manually.