Is there an (idiomatic) way of testing the result of an IO function in Clojure?

377 Views Asked by At

I have a function that saves some text to a file:

(defn save-keypair
  "saves keypair to ~/.ssb-clj/secret"
  [pair file-path]
  (let [public-key-string (->> (:public pair) (.array) (byte-array) (b64/encode) (bs/to-string))
        secret-key-string (->> (:secret pair) (.array) (byte-array) (b64/encode) (bs/to-string))]
    (spit file-path (str "Public Key: " public-key-string))
    (spit file-path (str "\nPrivate Key: " secret-key-string) :append true)))

It works fine (currently checking via just opening the file and looking at it myself). However, I'd like to write an actual test to check that everything is working correctly. Is there an idiomatic way of doing this in Clojure?

2

There are 2 best solutions below

0
On

Look into using with-redefs, as part of your unit tests. In your case, you probably want to merge the writing of the public and private keys into a single form which we can exploit for the test:

;; compute public-key-string and private-key-string as before
(let [contents (format "Public Key: %s\nPrivate Key: %s"
                        public-key-string secret-key-string)]
 (spit file-path contents)

A test could be something like:

(deftest saving-keypair
  (testing "Successful save"
    (let [file-mock (atom nil)]

      ;; During this test we redefine `spit` to save into the atom defined above
      (with-redefs [spit (fn [path data] (reset! file-mock {:path path :data data}))]

        ;; Perform IO action
        (save-keypair "~/.ssb-clj/secret" {:public "XXXX" :private "YYYYY"})

        ;; Test if the expected data was saved in the file-mock
        (is (= {:path "~/.ssb-clj/secret" :data "Public key: XXXYYYZZZ\nXXXYYYZZ"}
               @file-mock))
0
On

Use Java interop with File

https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/io/File.html

In particular, see File.createTempFile() and either file.delete() or file.deleteOnExit(). So you create a temp file, use that in your unit test, reading the file that you just wrote and verifying contents. Then either delete the file explicitly (ideally inside of try/finally) with auto-delete as a backup.

Depending on how you set up the expected results in your tests, you may find the following useful:

These helper functions are especially useful for text file output, where the presence of a trailing newline char can be OS dependent. They are also helpful to ignore differences due to the "newline" being CR, CR/LF, or LF.