One way to make your Clojure code easier to test and to reason about is to quarantine your IO. Amongst your team decide where you think the acceptable boundaries for IO are, and then don’t do any IO except in those layers. In my opinion the place to do IO is in the service layer, as described in Amit Rathore’s talk at Clojure/West, Domain-Driven Design With Clojure.
What this can lead to is code patterned where instead of executing IO straight away, you instead compute IO IOUs: a pure function determines that some piece of IO should occur, and returns that fact encoded in some form of data.
A Basic Example
Say we have a map that tracks the inventory changes at a warehouse. It typically looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
Then at a later stage of processing this code, at an outer layer that deals with IO, we can look over the actions on the inventory map and processes them accordingly.
Ramifications of This Style
A cool side effect of this is that if you want to change how your IO gets evaluated it will all be in a handful of well-defined places. This makes switching out implementation details easier. Also, it makes converting to asynchronous IO more straightforward if the IO becomes a blocker to the performance of the system.
Your code becomes less coupled to implementation details of your IO, and instead talks more about the computed intended effects. Unit testability of the system goes up, because everything except the service layer, is just pure functions and data.