Dry::Web::Container yielding different objects with multiple calls to resolve

290 Views Asked by At

I'm trying write a test to assert that all defined operations are called on a successful run. I have the operations for a given process defined in a list and resolve them from a container, like so:

class ProcessController
  def call(input)
    operations.each { |o| container[o].(input) }
  end

  def operations
    ['operation1', 'operation2']
  end

  def container
    My::Container # This is a Dry::Web::Container
  end
end

Then I test is as follows:

RSpec.describe ProcessController do
  let(:container) { My::Container } 

  it 'executes all operations' do
    subject.operations.each do |op|
      expect(container[op]).to receive(:call).and_call_original
    end

    expect(subject.(input)).to be_success
  end
end

This fails because calling container[operation_name] from inside ProcessController and from inside the test yield different instances of the operations. I can verify it by comparing the object ids. Other than that, I know the code is working correctly and all operations are being called.

The container is configured to auto register these operations and has been finalized before the test begins to run.

How do I make resolving the same key return the same item?

1

There are 1 best solutions below

0
On BEST ANSWER

TL;DR - https://dry-rb.org/gems/dry-system/test-mode/


Hi, to get the behaviour you're asking for, you'd need to use the memoize option when registering items with your container.

Note that Dry::Web::Container inherits Dry::System::Container, which includes Dry::Container::Mixin, so while the following example is using dry-container, it's still applicable:

require 'bundler/inline'

gemfile(true) do
  source 'https://rubygems.org'

  gem 'dry-container'
end

class MyItem; end

class MyContainer
  extend Dry::Container::Mixin

  register(:item) { MyItem.new }
  register(:memoized_item, memoize: true) { MyItem.new }
end

MyContainer[:item].object_id
# => 47171345299860
MyContainer[:item].object_id
# => 47171345290240

MyContainer[:memoized_item].object_id
# => 47171345277260
MyContainer[:memoized_item].object_id
# => 47171345277260

However, to do this from dry-web, you'd need to either memoize all objects auto-registered under the same path, or add the # auto_register: false magic comment to the top of the files that define the dependencies and boot them manually.

Memoizing could cause concurrency issues depending on which app server you're using and whether or not your objects are mutated during the request lifecycle, hence the design of dry-container to not memoize by default.

Another, arguably better option, is to use stubs:

# Extending above code
require 'dry/container/stub'
MyContainer.enable_stubs!
MyContainer.stub(:item, 'Some string')

MyContainer[:item]
# => "Some string"

Side note:

dry-system provides an injector so that you don't need to call the container manually in your objects, so your process controller would become something like:

class ProcessController
  include My::Importer['operation1', 'operation2']

  def call(input)
    [operation1, operation2].each do |operation|
      operation.(input)
    end
  end
end