Hello, is there a recommended method for “overriding” an implementation of a specific key? E.G. I have a “rollbar/client” that I want to override in our tests framework:
(defmethod ig/init-key :rollbar/client [_ {:keys [token environment git-commit-hash]}]
(let [r (r/client token {:environment environment
:code-version git-commit-hash})]
(r/setup-uncaught-exception-handler r)
(->Rollbar r)))
With a test version:
(defmethod ig/init-key :rollbar/test-client [_ {:keys [token environment git-commit-hash]}]
(reify IRollbar
(notify [_ level exception]
nil)))
Some ideas I had were:
1. passing in an extra arg into :rollbar/client
that could just do the mocking there
2. redeclaring the ig/init-key
:rollbar/client
as :rollbar/test-client
at test runtime
3. could use a “dispatch key” that the main and test implementation could potentially derive from? ah thanks for the callout!
This is an interesting problem that we've spent quite some time investigating in the team I'm on. We've gone through all of the options you mentioned, but TL;DR: we ended up going with option 1.
There are pros and cons to all of them, as I'm sure you've already discovered. If you'd like more details from our experience, I'd be happy to answer specific questions 🙂
Thanks for getting back to me! Yeah it’s pretty interesting, maybe I’ll do some combination of the three to solve it. The only thing I worry about with something too general is it being hard to understand later on…
I guess simple is better
Something else to consider is variability of mock/test implementations between tests. In some tests you may need a component to react in a different way than the default. How will you "patch" that behavior? For that we actually use some tooling for option 3. Option 2 causes more problems than it solves.
The "relevant tooling" is this: https://www.sweettooth.dev/endpoint/dev/systems/index.html#_the_esshrubbery_mock_init_key_alternative
wow thanks for the link! Definitely seems relevant to what I am grappling with
Yeah clearly you have thought all about this 🙂
Even that is an encumbering solution, but it's better than not being able to swap out components for custom (reified) mock ones
it seems nice to me!
Yeah so we just assoc-in
into the prepped config to mock components
this has the problem that we really only can mock the “value” of a duct component. We can only override the reference to another component not the actual component itself.
Each service has it’s own mock system fn.
I am trying to figure out a way to include a :test
profile that would just override certain components for every service’s tests.
I definitely like the data defined mocked components!
This touches on it: https://www.sweettooth.dev/endpoint/dev/systems/index.html#_testing
We have (use-fixtures :each (eth/system-fixture :test))
at the top of each file that needs to use the :test
config
which is define in a completely separate test.edn
That file contains :mock? true
configs to the components that should be mocked out by default
And most of the mockable components are init'ed like this:
(defmethod ig/init-key ::client
[_ {:keys [mock?] :as config}]
((if mock? mock-ecs-client live-ecs-client) config))
mmm hmm
and the sweet tooth code is responsible for inserting the :mock? true
onto the component?
It's only when we want custom behavior in a test, that we use eth/with-custom-config
with es/replacement
(for custom, reified mock objects) or es/shrubbery-mock
(for a "general" mock object)
No
We have :mock? true
in test.edn
sorry test.edn
is a duct profile?
E.g. above client is configured like this in test.edn
:
foo.components.ecs/client {:mock? true}
Yes it is
:duct.profile/test #duct/include "test.edn"
And in duct_hierarchy.edn
: :duct.profile/test [:duct/profile]
right
Yeah so I have my test.edn
profile also and like the simplicity of :mock? true
I was getting into trying to allow general dependency injection / overriding but that seems overly complex and hard to reason about for these fairly simple general cases.
If I were to do it, your alternative init-key
method seems pretty spot on
anyways, thanks for your help!
👍
But don't go with the ::test-component
(option 2) strategy. The major drawback of that is that you need to somehow replace all ig/ref
s to your original component, with ig/ref
s to the new one. You could add an intermediate component that switches between the two, but then you're practically back to option 3, only with an even more complicated config