Elmは純粋関数型プログラミング言語なので基本的にはアプリケーションを構成するすべての関数はテスタブルであるが、唯一Cmd型だけはテストすることができない。
たとえば以下のようなupdate関数においてPersistToStorage
のメッセージが渡された際に必ずストレージへの保存を実行するCmdが発行されているかどうかをテストしたいとする。
update : Msg -> Model -> ( Model, Cmd msg ) update msg model = case msg of PersistToStorage value -> ( Saving, persistToStorage value ) -- ... -- ports port persistToStorage : Encode.Value -> Cmd msg
ElmアプリケーションにおいてCmdは一度ラップされてしまうと、中のmsgを取り出すことはできない。またカスタム型であるためelm-testにおいて等価チェックのアサーションも使うことができない。以上の理由からCmdはテスタブルな型ではないということが分かる。
もちろん、CmdをテストせずModelの状態のみをチェックすることで実質副作用が発行されていることと同義とすることも可能である。Elmアプリケーションにおいて副作用というのはアプリケーションの状態に直接的に影響を与えるものではないし、そもそも「副作用が発行されているかどうかをテストするべきなのか?」という疑問は常に持っておくべきだと言える。
しかし、どうしても副作用をテストしたいという場合にはInterpreter Patternを用いることで副作用をテスタブルにすることができる。
Interpreter Patternとは
Interpreter Patternはざっくりいうと「アプリケーションの処理と実装を分離する」ためのパターンにあたる。ScalaやHaskellなどの関数型プログラミング言語の文脈においては有名なパターンで、計算機の実装などがよくある例である。
より具体的な実装方法で言うと「アプリケーションがどう動くか」を型など純粋なもので表現し、その型を解釈(Interpret)する別の関数に副作用の発行を移譲することで副作用のある部分と純粋な部分を切り離すという形になる。
ElmにおけるInterpreter Pattern
さて、ElmにおいてInterpreter Patternはどのような実装になるかというと、ただ単にupdate関数の副作用を自前で定義したカスタム型で置き換えるだけである。こうすることでupdate関数は完全にテスタブルな型のみを返すようになる。
update : Msg -> Model ( Model, ExCmd ) update msg model = case msg of PersistToStorage value -> ( Saving, ExPersistToStorage value ) -- ... type ExCmd = ExPersistToStorage Encode.Value
そして、ExCmdを解釈してCmd型の発行を行うInterpreterとなるtoCmd関数を用意し、main関数の呼び出し時にupdate関数と接続する
toCmd : ExCmd -> Cmd msg toCmd exCmd = case exCmd of ExPersistToStorage value -> persistToStorage value main : Program () Model Msg main = Browser.element { init = init , view = view , update = (\msg model -> Tuple.mapSecond toCmd <| update msg model) , subscriptions = \_ -> Sub.none }
これでCmd型に依存するのはtoCmd関数だけになったため、もともとの目的であった「update関数はPersistToStorage
のメッセージでストレージへの保存を実行するCmdを発行しているか」を以下のようにテストすることができる。
suite : Test suite = test "update" <| \_ -> init |> update (PersistToStorage "this is an apple") |> Tuple.second |> (\exCmd -> case exCmd of ExPersistToStorage _ -> True _ -> False ) |> Expect.equal True
これでupdate関数を完全にテスタブルなものにすることができた。
余談
Elmにおいてはアプリケーションでランタイムエラーが起きることはないため、どちらかといえばテスタビリティを上げるという観点でInterpreter Patternのような設計を行うことが多い。
見方を変えれば、Elmアプリケーションの実行を司るElmカーネルそのものがJavaScriptとの連携を行うInterpreterとしての役割を果たしているため、他の言語におけるInterpreter Patternのように「実行時エラーが起きる可能性があるレイヤとそうでないレイヤを分離する」というような実行時安全性を維持するための分離はElmにおいて全くもって必要ない。
もちろんJavaScriptやTypeScriptでは必要ではあるが。