Dry::Web::Container 通过多次调用解析产生不同的对象

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

我正在尝试编写一个测试来断言在成功 运行 时调用了所有已定义的操作。我在列表中定义了给定进程的操作,并从容器中解析它们,如下所示:

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

那我测试如下:

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

这失败了,因为从 ProcessController 内部调用 container[operation_name] 和从测试内部调用 container[operation_name] 会产生不同的操作实例。我可以通过比较对象 ID 来验证它。除此之外,我知道代码工作正常并且所有操作都被调用。

容器配置为自动注册这些操作,并在测试开始前完成运行。

如何解析同一个键 return 同一个项目?

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


您好,要获得您要求的行为,您需要在向容器注册项目时使用 memoize 选项。

请注意,Dry::Web::Container 继承了 Dry::System::Container,其中包括 Dry::Container::Mixin,因此尽管以下示例使用 dry-container,但它仍然适用:

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

但是,要从 dry-web 执行此操作,您需要 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.

记忆可能会导致并发问题,具体取决于您使用的是哪个应用程序服务器以及您的对象在请求生命周期中是否发生变化,因此 dry-container 的设计默认不记忆。

另一个可以说是更好的选择是使用 stubs:

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

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

旁注:

dry-system 提供了一个 injector 这样你就不需要在你的对象中手动调用容器,这样你的进程控制器就会变成这样:

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

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