This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2021-11-29
Channels
- # adventofcode (20)
- # announcements (6)
- # asami (13)
- # babashka (9)
- # beginners (80)
- # calva (53)
- # cider (16)
- # clj-kondo (24)
- # cljs-dev (40)
- # clojure (13)
- # clojure-australia (9)
- # clojure-europe (117)
- # clojure-india (3)
- # clojure-italy (4)
- # clojure-nl (5)
- # clojure-sg (1)
- # clojure-spec (4)
- # clojure-uk (6)
- # clojurescript (6)
- # cursive (41)
- # datalog (5)
- # datomic (11)
- # emacs (9)
- # events (1)
- # fulcro (46)
- # integrant (31)
- # jobs (1)
- # kaocha (1)
- # lein-figwheel (3)
- # lsp (2)
- # meander (3)
- # missionary (4)
- # pathom (6)
- # portal (84)
- # re-frame (3)
- # remote-jobs (1)
- # reveal (2)
- # shadow-cljs (36)
- # tools-build (3)
- # xtdb (17)
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.
Use a different implementation in your tests. JUXT had a great blog about this approach recently: https://www.juxt.pro/blog/abstract-clojure
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.
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.
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
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
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.
So you can use the exact same system config, but you need to require different implementations to construct it.
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.
@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.
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
@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.
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
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.
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
).
Regex searches can also work, but it's not what most people reach for automatically. Could work, though
In practice I can just attest to derived keys causing confusion, even though it's quite a "Clojure-elegant" solution
I see, I'd never looked at the ig/load-namespaces function before, I wasn't really aware of it
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:
(defmethod ig/init-key ::manager [_ {:keys [mock?] :as config}]
((if mock? mock-dns-manager live-dns-manager) config))
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
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)))
I dunno. I've had good experiences with the derived keys.
Although I'll admit I don't rely on code being loaded by the config.
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.
Should all just work:tm: