duct

2021-04-14T17:36:54.017900Z

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?

2021-04-15T15:02:43.025700Z

ah thanks for the callout!

walterl 2021-04-14T21:06:18.018400Z

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.

walterl 2021-04-14T21:07:23.018600Z

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 🙂

2021-04-14T21:35:16.018800Z

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…

2021-04-14T21:36:54.019Z

I guess simple is better

walterl 2021-04-14T21:59:00.019200Z

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.

2021-04-14T23:01:38.019600Z

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

2021-04-14T23:09:02.019800Z

Yeah clearly you have thought all about this 🙂

walterl 2021-04-14T23:14:13.020Z

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

2021-04-14T23:15:04.020200Z

it seems nice to me!

2021-04-14T23:19:50.020400Z

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.

2021-04-14T23:20:47.020600Z

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.

2021-04-14T23:22:42.020800Z

I definitely like the data defined mocked components!

walterl 2021-04-14T23:25:16.021300Z

This touches on it: https://www.sweettooth.dev/endpoint/dev/systems/index.html#_testing

walterl 2021-04-14T23:26:37.021500Z

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

walterl 2021-04-14T23:26:51.021700Z

which is define in a completely separate test.edn

walterl 2021-04-14T23:27:18.021900Z

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

walterl 2021-04-14T23:28:18.022100Z

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

2021-04-14T23:29:11.022300Z

mmm hmm

2021-04-14T23:29:48.022500Z

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

walterl 2021-04-14T23:29:52.022700Z

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)

1👍
walterl 2021-04-14T23:29:57.022900Z

No

walterl 2021-04-14T23:30:15.023200Z

We have :mock? true in test.edn

2021-04-14T23:30:57.023400Z

sorry test.edn is a duct profile?

walterl 2021-04-14T23:31:21.023600Z

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

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

walterl 2021-04-14T23:32:13.023800Z

Yes it is

walterl 2021-04-14T23:32:36.024Z

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

walterl 2021-04-14T23:32:58.024200Z

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

2021-04-14T23:33:04.024400Z

right

2021-04-14T23:34:34.024600Z

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.

2021-04-14T23:35:51.024800Z

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

2021-04-14T23:37:50.025Z

anyways, thanks for your help!

walterl 2021-04-14T23:38:02.025200Z

👍

walterl 2021-04-14T23:40:00.025400Z

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