Fork me on GitHub
#integrant
<
2021-11-29
>
Max17:11:12

This is probably a dumb question, but how do you replace the implementation of a component, e.g. in a test? With Component you can just build a system map with a different defrecord for the component’s value, but since integrant uses multimethods based on the component key, I don’t see a straightforward way to build systems with alternative component implementations.

thom17:11:52

Use a different implementation in your tests. JUXT had a great blog about this approach recently: https://www.juxt.pro/blog/abstract-clojure

thom17:11:32

So each component has a protocol defining its interface, and in your tests you can use a topology with mock versions for whatever needs to be mocked.

thom17:11:22

You can even reuse the configuration of your system here, just require different namespaces for the implementations, but often you'll be testing against a partial system anyway so I find it useful to have a test fixture to start and stop a custom system within a test namespace.

Max18:11:25

So then you have multimethods wrapped around protocols? I feel like I’m missing something here, but that sounds a lot like Component with extra steps

Max18:11:11

I know you can have an entire alternate system for testing, but one of the big advantages people talk about for DI is being able to swap impls out ad-hoc as needed

thom18:11:00

You want all you code that talks to a component to do so through a protocol. That's the first step to being able to swap out implementations. Different versions of a multi method with the same dispatch value will then return those different implementations. In tests, don't require the real implementation, just the test one.

thom18:11:21

So you can use the exact same system config, but you need to require different implementations to construct it.

thom18:11:15

Of course you can test many things entirely outside of integrant if you want. Your components take each other as parameters so you can just supply the test versions directly. But I find Integrant makes this a little easier.

thom18:11:13

(I may just be repeating myself here, tell me if that's still not clear!)

Joshua Suskalo17:12:37

@U01EB0V3H39 with integrant you can use clojure's hierarchies to dispatch, which means you could depend on a ref to :myapp/something-generic and then have like :myapp.something-generic/prod for the real version and :myapp.something-generic/test for the test version, and put a derive call to make both of those derive from the generic thing you depend on.

Joshua Suskalo17:12:06

Then you have a production system map which provides the :myapp.something-generic/prod as an actual key, and depend on :myapp/something-generic, and then in the test version you dissoc off the /prod version and assoc in your /test version

walterl23:12:49

@U5NCUG8NR While that https://gist.github.com/walterl/67a2690ebea01990b7c64432a0ea10df (I had to go try it out again), it has the downside that you can no longer search for references to :myapp/something-generic in the config. In large configs that can be quite confusing.

Joshua Suskalo23:12:47

That's why I recommend the type of naming scheme I just mentioned. You could make a regex search for myapp.something-generic and it'd match both myapp/something-generic and myapp.something-generic

Joshua Suskalo23:12:50

So instead of having ::base-widget derived from by ::prod-widget, you have :myapp/base-widget and :myapp.base-widget/prod or whatever. Normally I come up with better names for these, but that's the idea.

walterl23:12:59

Sure, one could do that, but that has other downsides, like not being able to use namespaced (with "real" namespaces) keys for components, meaning you have to do manual component loading (a la ig/load-namespaces).

walterl23:12:13

Regex searches can also work, but it's not what most people reach for automatically. Could work, though

walterl23:12:08

In practice I can just attest to derived keys causing confusion, even though it's quite a "Clojure-elegant" solution

Joshua Suskalo23:12:14

I see, I'd never looked at the ig/load-namespaces function before, I wasn't really aware of it

walterl23:12:38

We've tried a few different solutions to @U01EB0V3H39's problem, and couldn't really find any "good" solution. One of the simpler ones (at the cost of some scaffolding) moves the choice of component implementation into the component init-key method:

walterl23:12:52

(defmethod ig/init-key ::manager [_ {:keys [mock?] :as config}]
  ((if mock? mock-dns-manager live-dns-manager) config))

Max00:12:53

At that point I kind of wonder if you’d be better off with Component. Then you could just make a different defrecord implementing the protocol

👍 1
walterl00:12:53

Or, if you need to be able to specify custom (`reify`-ed) alternative impementations:

(defmethod ig/init-key ::manager [_ {:keys [alt] :as config}]
  ((if alt alt (live-dns-manager config)))

Joshua Suskalo00:12:43

I dunno. I've had good experiences with the derived keys.

Joshua Suskalo00:12:58

Although I'll admit I don't rely on code being loaded by the config.

Joshua Suskalo00:12:38

You could make similar naming schemes that work fine though by having like :myapp/some-widget being the key derived from, the namespace is myapp.some-widget, and it defines an implementation with :myapp.some-widget/prod, and the ig/load-namespaces will still load everything correctly because it tries to load both the namespace, and also the concatenation of the namespace with the name.

Joshua Suskalo00:12:43

Should all just work:tm:

walterl00:12:32

Interesting :thinking_face:

colinkahn02:12:41

You can wrap ig/init-key when calling ig/build and selectively init with mocks what you want and then fall back the original multimethod for the rest.

walterl16:12:48

Nice! I was unaware of ig/build