Tuesday, February 10, 2015

Fun with Optional Typing: cheap mocking

For unit tests, it's handy to mock out side-effecting functions so they don't slow down tests.[1] Clojure has an easy way to do this: use with-redefs to override function definitions, and then any code within the with-redefs block uses those definitions instead.

To verify the input of the side-effecting function, I can override it with something that throws an exception if the input is wrong.[2] A quick way to do that is to check the input against a schema.

That turns out to be kinda pretty. For instance, if I need to override this function fetch-orders, I can enforce that it receives exactly the starting-date I expect, and a second argument that is not specified precisely, but still meets a certain condition.

(with-redefs [fetch-orders (s/fn [s :- (s/eq starting-date)
                                  e :- AtLeastAnHourAgo]
... )

Here, the s/fn macro creates a function that (when validation is activated[3]) checks its input against the schemas specified after the bird-face operator. The "equals" schema-creating function is built-in; the other I created myself with a descriptive name. The overriding function is declarative, no conditionals or explicit throwing or saving mutable state for later.

If I have a bug that switches the order of the inputs, this test fails. The exception that comes out isn't pretty.
expected: (= expected-result (apply function-under-test input))
  actual: clojure.lang.ExceptionInfo: Input to fn3181 does not match schema: [(named (not (= #<DateTime 2014-12-31T23:55:00.000Z> a-org.joda.time.DateTime)) s) nil]
Schema isn't there yet on pretty errors. But hey, my test reads cleanly, it was simple to write, and I didn't bring in a mocking framework.

See the full code (in the literate-test sort of style I'm experimenting with) on github.

[1] for the record, I much prefer writing code that's a pipeline, so that I only have to unit-test data-in, data-out functions. Then side-effecting functions are only tested in integration tests, not mocked at all. But this was someone else's code I was adding tests around.

[2] Another way to check the output is to have the override put its input into an atom, then check what happened during the assertion portion of the test. Sometimes that is cleaner.

[3] Don't forget to (use-fixtures :once schema.test/validate-schemas) 

No comments:

Post a Comment