rubydry-rb

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


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?


Solution

  • 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