How to unit test Service using dry-auto_inject

Hi,

I am experimenting with Inversion Of Control with dry-container & dry-auto_inject, see https://discuss.dry-rb.org/t/examples-of-how-to-use-dry-container-dry-auto-inject/82/1

This is probably a question for @solnic again :wink:

Now that I have a test suite with integration tests, I would like to add an Unit Test.

My current WIP:

# ------------------ Implementation
  class SimpleHashCredentialsCache
    include AutoInject[:get_customer_code, :get_credentials]

    def get
      get_credentials.call(get_customer_code)
    end
  end
end

#------------------ UNIT TEST
# TODO Move this to a 'spec/unit_helper'
$LOAD_PATH.unshift File.expand_path('../../../lib', __FILE__)

require 'dry-auto_inject'

describe "'Credentials cache' service" do
  subject do
    # Problem when running full test suite, AutoInject is already defined:
    # warning: already initialized constant ClusterPlug::AutoInject
    #
    # Is it possible to write unit test for classes which use AutoInject?
      AutoInject = Dry::AutoInject({
        get_customer_code: 'XX_0001',
        # TODO Replace this by Mock to run assertion on customer_code param
        get_credentials: ->(param_not_used) {
          ClusterPlug::Credentials.new(
            host: 'localhost',
            port: 1,
            username: 'tom',
            password: 'x-tom',
            database: 'postgres'
          )
        },
      })
    end

    # Must require after AutoInject module has been defined
    require 'simple_hash_credentials_cache'
    SimpleHashCredentialsCache.new
  end

  context "first call for a given customer code" do
    it "gets a Credentials Value Object" do
      actual_credentials = subject.get

      expect(actual_credentials.host).to eq('localhost')
    end
  end
end

This runs fine when I run only this unit test file. But I run all the files, I get a warning:
already initialized constant ClusterPlug::AutoInject

I am wondering, whatā€™s the best practice here? Create an AutoInject module whose life span is only for the duration of the example?

I quite like the idea of not loading dry-container and the default config of dependencies for my gem. But if I end up with some very complex setup in the unit test, maybe I should be pragmatic and just load all the things ā€¦

I will do a bit of research and post my update here.

Cheers,

JM

We use dry-component which takes care of this kind of stuff; it lazy-loads required components so in unit tests the container is empty and it loads stuff on demand. For now I would recommend loading everything, but in unit tests if you have a dependency you probably want to mock it and just pass a mock to the object-under-testā€™s constructor.

Iā€™ll be working on an improved version of dry-component which we just decided to rename to dry-system. Iā€™ll make sure itā€™s easy to drop into an existing project, it takes care of many nasty things for you, like setting up $LOAD_PATH, resolving dependencies on demand, booting 3rd party code in complete isolation and so on.

Thanks @solnic. I will wait for dry-system before I change my current container then.

Just FYI, if you were doing everything in complete isolation, hereā€™s how thatā€™d look:

RSpec.describe SimpleHashCredentialsCache do
  subject(:credentials_cache) {
    described_class.new(
      get_customer_code: get_customer_code,
      get_credentials: get_credentials
    )
  }

  let(:get_customer_code) { double("get_customer_code", call: "foo") }
  let(:get_credentials) { double("get_credentials", call: "bar") }

  it "works" do
    # do stuff here
  end
end

This uses RSpec doubles, but you could also pass simple proc objects (e.g. let(:get_customer_code) { -> { "foo" } }) as well.

Another thing you could do, instead of building a replacement injector object, is use the stubbing support in dry-container (used inside dry-component). We havenā€™t documented it yet, but you can see the code here and a usage example from the PR here.

No need to wait anymore dry-rb - Introducing dry-system :slight_smile:

a bit late to the party. This notification was at the bottom of my ā€œupdatesā€. Thank you :slight_smile:

This is great, but itā€™s still not clear to me how to write a unit test. Do you have an example of that?

Does my reply above show you well enough? Or is there something else youā€™d be looking to do with your unit tests?

Iā€™ve tested this and it does not seem to work, here is my class that has stuff injected into it https://github.com/alexandru-calinoiu/dry-rb-test/blob/684e7ff52e2dd12043d9fd4c96ae76fd53d34e2a/lib/dry/test/commands/create_article.rb and here is the test for it https://github.com/alexandru-calinoiu/dry-rb-test/blob/684e7ff52e2dd12043d9fd4c96ae76fd53d34e2a/spec/dry/test/commands/create_article_spec.rb

Test running fails unsurprisingly with ArgumentError: wrong number of arguments (given 2, expected 0)

For this to work will mean that dry would have to create a ctor for my class, or shall I add this for testing purposes?

I think the reason itā€™s not working is because youā€™re passing regular positional arguments to your classā€™ initializer, when by default dry-auto_inject sets up an initializer using keyword args. So if you change your subject to this:

subject(:create_article) do
  described_class.new(validate_articles: validate, persist_articles: persist)
end

Does it work?

You are right (of course),

Can you point me to the part of the docs that describes this, please?

Docs might be out of date at the moment, sorry about that! Would gratefully accept any improvements youā€™d like to submit :slight_smile: The repo is over at https://github.com/dry-rb/dry-rb.org.

Thank you Tim for your reply. Somehow, I was not notified.

At the end, I wrote a very simple Dependency injections framework:

  class Dependencies
    def initialize
      @dependencies = {}
    end

    def register(key, instance)
      dependencies[key] = instance
    end

    def resolve(key)
      dependencies[key]
    end

    private

    attr_reader :dependencies
  end

And injected dependencies manually. It was a small project with 4-5 components sot it was enough.

I did end up using dry-initializer though, with better success. And I even contributed to a feature;)

To conclude, I am not ready to use dry-system and dry-auto_inject, especially not dry-auto_inject alone. Might give them another try in another project some day ā€¦ Or become an elixir developer :wink: