0

I have the following pair of functions used for searching upwards for a configuration file:

(defn has-config? [path]
  contains? (set (.list path)) "my-config")

(defn find-config-path [path]
  (cond
    (has-config? path) path
    (= "/" path) (throw (Exception. "File not found"))
    :else (recur(.getParentFile path))))

Here, the argument path is a return value of clojure.java.io/file

To make unit testable, I would usually separate the side effects, in this case I/O, from the logic. Following this approach, the last function would receive the contents of the directory instead of the generating them inside.

My problem is in the recursion call, which should pass the contents of the parent directory. In order to do this, I/O is necessary and I cannot seem to come to a good way to decouple this from the logic. Is this the right approach or am I missing something obvious here?

2
  • What part of this are you thinking of as your logic? Sep 14, 2019 at 10:47
  • As logic I would consider making the decision on how to proceed based on the contents of the current path (return a boolean, throw an error or recurse)
    – pob
    Sep 14, 2019 at 11:50

1 Answer 1

2

I am not fluent in Clojure, but why not pass the function for finding the parent path as a parameter to find-config-path, where .getParentFile is used as default value? Something along the lines of

(defn find-config-path 
      ([path] (find-config-path path .getParentFile))
      ([path gpf] (cond (has-config? path) path
              (= "/" path) (throw (Exception. "File not found"))
               :else (recur(gpf path))))

Now you can pass a "mock" function as a replacement for .getParentFile as a parameter for testing purposes.

4
  • Thank you, I have considered something along those lines. In fact, I could treat the path itself as an "object" that could be mocked. What I am wondering is if there is a way to decouple the decision making (as a pure function) from the side-effects (I/O).
    – pob
    Sep 14, 2019 at 11:57
  • @pob: if there is a way to decouple the decision making (as a pure function) from the side-effects (I/O) - can you explain in what way my suggestion does not exactly provide this? You now can pass a side-effect free mock function for gpf, which provides some dummy parent files/folders without doing any real I/O. What am I missing here?
    – Doc Brown
    Sep 14, 2019 at 12:04
  • You're missing nothing, you are correct in that the test function will be pure. I realized I didn't ask the right question. What I was looking for is for an approach that would leave me (perhaps) with two functions in the "production code", one pure and another one isolating the side-effects. For example, if I would like to search the configuration file only in one directory, I would have have one function for reading the contents of that directory, and a second one with the decision logic. I would only need to test the second one without the need for additional mocks.
    – pob
    Sep 14, 2019 at 13:21
  • @pob: I still don't get it - my suggestions contains two functions, find-config-path contains only the decision logic, and .getParentFile does the content reading. As an alternative, you could make find-config-path a function which operates on a lazy sequence of strings, and implement a second function which produces those strings from path as its parent folder names. Then you can pass the sequence into find-config-path as a parameter. But don't expect the code to look very different from the solution above.
    – Doc Brown
    Sep 14, 2019 at 16:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.