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.
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.
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.
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
Docs might be out of date at the moment, sorry about that! Would gratefully accept any improvements youād like to submit 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