Mocking Without Dependency Inversion

Just a few thoughts and musings.

September 8, 2021

A few days ago I made the following observation about mocking external library functions in clojure:

An interesting distinction between these two testing approaches is this: in the polymorphic, interface-driven approach, the production code references the abstraction of the interface. In the clojure approach, the production code references the concrete, external function directly and the test sneakily patches it with a fake/stub/mock thing.

Since writing that article I've gone back to code that I had written before discovering the with-redefs approach to mocking in Clojure. At that time, I was trying to separate things that were easy to test from things that were not easy to test (and which I didn't know how to redefine/mock). In some cases I would receive the untestable thing/service as a function argument to the SUT (system under test) as a way to open up a seam for testing. Where this wasn't plausible I was mostly just leaving the things I didn't know how to redefine/mock untested, which wasn't good.

Fast-forward to the present and I've been using mocks and with-redef whenever needed to get something under test. With a bit of design effort I'm able to get anything under test, but I'm noticing a few interesting things about the size of the 'units' under test.

Before coming to Clojure I experimented a great deal with different 'unit' sizes in tests. I found great value in limiting the access my tests had to few well-defined high-level entry points in the production code. Whenever possible, the tests would exercise larger amounts of production code so that the internals of that production code could be refactored without the tests even knowing anything about the changes. These were still 'unit' tests in the sense that they didn't cross process boundaries (like talking to disk or hopping over a network).

But now in Clojure I find myself writing tests for very low-level functions. It's becoming more and more common for any test that invokes code at a higher level to need certain calls to be mocked/redefined (like database wrapper functions). This seems to be a consequence of the code not really abiding the dependency inversion principle and relying on the mocking framework to do lots of heavy lifting.

I'm hoping I uncover better ways of testing Clojure systems as I see more code and acquire more skill with the language and the libraries in play.

-Michael Whatcott