How to unit test Service using dry-auto_inject


#1

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


#2

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.


#3

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


#4

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.


#5

No need to wait anymore http://dry-rb.org/news/2016/08/15/introducing-dry-system/ :slight_smile:


#6

a bit late to the party. This notification was at the bottom of my “updates”. Thank you :slight_smile:


#7

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


#8

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


#9

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?


#10

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?


#11

You are right (of course),

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


#12

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.


#13

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: