dry-rb

dry-rb related notes.

mocking dependencies in specs

  1. https://github.com/dry-rb/dry-container/pull/11#issuecomment-184765175
  2. https://github.com/dry-rb/dry-container/blob/master/lib/dry/container/stub.rb
  3. https://discuss.dry-rb.org/t/how-to-unit-test-service-using-dry-auto-inject/88/4
  4. https://discuss.dry-rb.org/t/example-changes-of-dependencies/50

setup

NOTE: this setup is required only when using container stubs.

config/environment.rb:

# Load and finalize DI container
require_relative '../system/my_app/container'

# Enable container stubs in test environment only
if Rails.env.test?
  require 'dry/container/stub'
  MyApp::Container.enable_stubs!
end

# Don't finalize container in test environment at all since
# it freezes container class and you cannot:
# - enable stubs at all if you do it after container finalization
#   (since it extends container class with `Dry::Container::Stub`)
# - stub anything in your specs
MyApp::Container.finalize! unless Rails.env.test?

TODO: in RB container is finalized unconditionally and container stubs still work even though container class is frozen - why?

usage

controller specs

the problem in controller specs is that you cannot stub specific container entry in before block because by this time controller object will have already been instantiated and injected with real objects. thus the only way to stub injected dependency in controller specs is to stub corresponding method in created PORO (see #2 below for details).

PORO specs

describe Item::Create do
  let(:operation) { described_class.new }

  let(:process_item) { double call: nil }
  before { MyApp::Container.stub('svcs.process_item', process_item) }
  before { operation.call }

  it { expect(process_item).to have_received(:call) }
end
describe Item::Create do
  let(:operation) { described_class.new }

  let(:process_item) { double call: nil }
  before { allow(operation).to receive(:process_item).and_return process_item }
  before { operation.call }

  it { expect(process_item).to have_received(:call) }
end

kwargs injection strategy is the default so pass a hash (source):

describe Item::Create do
  let(:operation) { described_class.new process_item: process_item }

  let(:process_item) { double call: nil }
  before { operation.call }

  it { expect(process_item).to have_received(:call) }
end

dry types from custom classes/models

module Types
  include Dry::Types.module

  DryUser = Dry::Types::Definition.new(User)
    .constructor { |v| v }
    .constrained(type: User)
end

here constrained method from dry-types is using built-in type? predicate from dry-logic. even constructor returns passed value without any processing it’s possible to omit it:

DryUser = Dry::Types::Definition.new(User).constrained(type: User)