integrant

Max 2021-11-29T17:35:12.068700Z

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.

walterl 2021-12-07T16:56:48.087100Z

Nice! I was unaware of ig/build

thom 2021-11-29T17:53:52.069500Z

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

thom 2021-11-29T17:54:32.070800Z

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.

thom 2021-11-29T17:58:22.073400Z

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.

Max 2021-11-29T18:34:25.073600Z

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

Max 2021-11-29T18:35:11.074500Z

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

thom 2021-11-29T18:38:00.078Z

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.

thom 2021-11-29T18:38:21.078800Z

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

thom 2021-11-29T18:39:15.080400Z

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.

thom 2021-11-29T18:53:13.081400Z

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

2021-12-07T02:51:41.086800Z

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.

2021-12-01T17:44:37.081700Z

@max.r.rothman 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.

2021-12-01T17:47:06.081900Z

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

walterl 2021-12-01T23:35:49.082100Z

@suskeyhose 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.

2021-12-01T23:36:47.082400Z

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

2021-12-01T23:39:50.082600Z

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.

2021-12-01T23:41:13.082800Z

@clojurians-slack100

walterl 2021-12-01T23:46:59.083Z

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

walterl 2021-12-01T23:47:13.083200Z

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

walterl 2021-12-01T23:48:08.083400Z

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

2021-12-01T23:51:14.083600Z

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

walterl 2021-12-01T23:57:38.083800Z

We've tried a few different solutions to @max.r.rothman'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:

walterl 2021-12-01T23:57:52.084Z

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

Max 2021-12-02T00:01:53.085300Z

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
walterl 2021-12-02T00:01:53.085500Z

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

2021-12-02T00:04:43.085800Z

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

2021-12-02T00:04:58.086Z

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

2021-12-02T00:06:38.086200Z

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.

2021-12-02T00:06:43.086400Z

Should all just work:tm:

walterl 2021-12-02T00:13:32.086600Z

Interesting 🤔