20 August 2021

Mocking With Speclj

A phrase which here means "designing functional code to be more testable".

As I've wrestled with an evolving tic-tac-toe game recently I've wanted to test certain components in isolation of other components, often those that are stateful or non-deterministic (like input coming from a user). In an object-oriented language like Java, or Go it's simply a matter of applying the Dependency Inversion Principle by putting those things behind interfaces and implementing 'fake' versions that allow you to decouple the component under test from its runtime dependencies.

In clojure I've been scratching my head, not knowing how to implement a 'fake' that returns pre-fabricated responses and/or records inputs provided to it. All of this feels very non-functional, requiring the updating of stateful references. Ew!

Well, it turns out that Speclj, the testing library I've been using, has everything you'd need to provide simple mocking capabilities. I had the good fortune to pair program with my mentor, Micah Martin, who also happens to be the author of Speclj. The following example is what he guided me toward.

Ok, suppose you are coding a tic-tac-toe game in clojure. You've worked out all of the grid mechanincs and rules which means you can identify end-of-game conditions and players can place marks on the board. When faced with the task of integrating all of that into a routine to play an entire game I came up with this:

(defn play [presenter grid mark player1-in player2-in]
  (do
    (presenter grid)
    (cond (boolean (winner grid)) (other mark) ; X or O won
          (empty? (available-cells grid)) nil  ; drawn game
          :else (recur presenter
                       (place mark (player1-in mark grid) grid)
                       (other mark)
                       player2-in
                       player1-in))))

It gets the job done, but it has issues.

  1. 5 input parameters to a function is a lot for a reader to process.
  2. Notice the side-effecty presenter call in the middle of the recursive algorithm.
  3. In order to test this properly you'd have to fake out both players and the presenter, a somewhat complicated and tall order.

What if the act of performing one turn was it's own method that received game state and returned an updated version of the game state?

(defn tick [{:keys [grid mark player1 player2] :as game-state}]
  (let [next-grid  (place mark (player1 mark grid) grid)
        winner     (winner next-grid)
        game-over? (or (empty? (:empty-cells next-grid))
                       (boolean winner))]
    (-> game-state
        (update :mark other)
        (assoc :grid next-grid)
        (assoc :player1 player2)
        (assoc :player2 player1)
        (assoc :winner winner)
        (assoc :game-over? game-over?))))

Bonus #1: This code does all of the updating of the grid but doesn't need to concern itself with the presenter. :)

Bonus #2: The game-state data structure is elegantly destructured in the function signature.

Bonus #3: Note the effective application of the 'thread-first' macro to really simplify what would be nested map update and assoc function calls. More on threading macros...

Here's how we went about testing this tick function:

(describe "Game Play"

  (with-stubs) ; <-- IMPORTANT

  (context "A Single Tick"
    (it "updates game state after one move"
      (let [player1    (stub :player1 {:return 1}) ; <-- IMPORTANT
            grid       (new-grid 3)
            game-state {:grid    grid
                        :mark    X
                        :player1 player1
                        :player2 :player2}
            result     (tick game-state)]
        (should-have-invoked :player1 {:with [X grid]}) ; <-- IMPORTANT
        (should= (place X 1 grid) (:grid result))
        (should= O (:mark result))
        (should= player1 (:player2 result))
        (should= :player2 (:player1 result))
        (should= false (:game-over? result))
        (should-be-nil (:winner result))))
    )
  )

I highlighted the elements of the test above that contribute to the mocking strategy.

  1. Declare that you are going to be stubbing something: (with-stubs)
  2. Create the stub and indicate its return value: (stub :player1 {:return 1})
  3. Assert that the stub was invoked with the correct input: (should-have-invoked :player1 {:with [X grid]})

Someday, I'll come to more fully understand the inner workings of those helpers. For now, I'll be content to just use them effectively.

Now for the game loop. Assuming you can lazily iterate a sequence of game ticks (which feed their results into subsequent calls to tick) you can write the following to invoke the presenter and end the game when appropriate:

(defn game-loop [presenter ticks]
  (loop [ticks ticks]
    (let [tick (first ticks)]
      (presenter (:grid tick))
      (if (:game-over? tick)
        (:winner tick)
        (recur (rest ticks))))))

With its corresponding tests:

(describe "Game Play"
  ... ; as previously seen

  (context "The Game Loop (multiple ticks)"

    (it "iterates until game is over"
      (let [ticks  [{:game-over? false :grid "grid1" :winner nil}
                    {:game-over? false :grid "grid2" :winner nil}
                    {:game-over? true, :grid "grid3" :winner O}] ; <-- GAME OVER!
            winner (game-loop (stub :presenter) ticks)]
        (should= O winner)
        (should-have-invoked :presenter {:with ["grid1"]})
        (should-have-invoked :presenter {:with ["grid2"]})
        (should-have-invoked :presenter {:with ["grid3"]})))
    )
  )

The only tiny snippet that remains from the original play method (still not under test, but we can live with that):

(defn play [presenter grid mark player1-in player2-in]
  (let [game-state {:grid    grid
                    :mark    mark
                    :player1 player1-in
                    :player2 player2-in}]
    (game-loop presenter (iterate tick game-state))))

The purpose of this function is to get the 5 parameters into the game-state structure which then makes it possible to conveniently iterate in a lazy fashion, allowing game-loop to short-circuit as already shown. Pretty slick if you ask me.