Functional Mocking

Abstract

Mocking is an infamous technique from object-oriented programming. The goal is to be able to test stateful systems in small pieces by simulating the behaviour of certain objects. The problem with mocking is that it usually requires heavyweight frameworks and clutters test code. There are countless rants on that topic, but this talk isn't one. Instead, we'll explore the functional approach in Haskell: Designing a small language supporting the desired behaviour, and then writing interpreters which can execute its semantics in various ways. Testing I/O code was never easier.

Recording

Events

Demo code

{-# LANGUAGE GADTs #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeOperators #-}

module Terminal where

import Control.Monad.Free (Free, liftF, iterM)
import Control.Monad.State
import Data.Functor.Coyoneda (Coyoneda (Coyoneda), liftCoyoneda)

type FreeC f = Free (Coyoneda f)

type f ~> g = forall a. f a -> g a

runFreeC :: Monad g => (f ~> g) -> FreeC f a -> g a
runFreeC f = iterM $ \(Coyoneda g i) -> f i >>= g

liftC :: f a -> FreeC f a
liftC = liftF . liftCoyoneda


data Terminal a where
  ReadLine :: Terminal String
  WriteLine :: String -> Terminal ()

type TerminalIO = FreeC Terminal

readLine    = liftC ReadLine
writeLine s = liftC (WriteLine s)


terminalToIO :: Terminal ~> IO
terminalToIO ReadLine = getLine
terminalToIO (WriteLine s) = putStrLn s

type MockTerminal = ([String], [String])

terminalToState :: Terminal ~> State MockTerminal
terminalToState ReadLine = do
  (i, o) <- get
  case i of
    h : t -> do
      put (t, o)
      return h
    [] ->
      return ""
terminalToState (WriteLine s) = do
  (i, o) <- get
  put (i, o ++ [s])


program :: TerminalIO ()
program = do
  line <- readLine
  writeLine line