Fork me on GitHub
#duct
<
2021-04-14
>
Spencer Apple17:04:54

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?

walterl21:04:18

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.

walterl21:04:23

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 🙂

Spencer Apple21:04:16

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…

Spencer Apple21:04:54

I guess simple is better

walterl21:04:00

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.

Spencer Apple23:04:38

wow thanks for the link! Definitely seems relevant to what I am grappling with

Spencer Apple23:04:02

Yeah clearly you have thought all about this 🙂

walterl23:04:13

Even that is an encumbering solution, but it's better than not being able to swap out components for custom (reified) mock ones

Spencer Apple23:04:04

it seems nice to me!

Spencer Apple23:04:50

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.

Spencer Apple23:04:47

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.

Spencer Apple23:04:42

I definitely like the data defined mocked components!

walterl23:04:37

We have (use-fixtures :each (eth/system-fixture :test)) at the top of each file that needs to use the :test config

walterl23:04:51

which is define in a completely separate test.edn

walterl23:04:18

That file contains :mock? true configs to the components that should be mocked out by default

walterl23:04:18

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))

Spencer Apple23:04:48

and the sweet tooth code is responsible for inserting the :mock? true onto the component?

walterl23:04:52

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)

👍 3
walterl23:04:15

We have :mock? true in test.edn

Spencer Apple23:04:57

sorry test.edn is a duct profile?

walterl23:04:21

E.g. above client is configured like this in test.edn:

foo.components.ecs/client {:mock? true}

walterl23:04:36

:duct.profile/test #duct/include "test.edn"

walterl23:04:58

And in duct_hierarchy.edn: :duct.profile/test [:duct/profile]

Spencer Apple23:04:34

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.

Spencer Apple23:04:51

If I were to do it, your alternative init-key method seems pretty spot on

Spencer Apple23:04:50

anyways, thanks for your help!

walterl23:04:00

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/refs to your original component, with ig/refs 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

Spencer Apple15:04:43

ah thanks for the callout!