Elm
Elm 2Day 10

ElmのCmdをスタブする

More than 1 year has passed since last update.

ElmのHttp処理部をスタブする
に対するアンサーです

要約

  • 元のMain.elmをスタブしたいって理由で変更入れたくない
  • updateから返却されるcmdを入れ替える

問題

Elmでアプリケーション書いているとリモートのWeb APIへリクエストを飛ばす部分がでてくる。
けれどチュートリアルの章を読むとHttpパッケージの関数を直接埋め込んで埋め込んである。この流儀に従ってそのまま実装していくと、elm-reactorで動かした際に、Web APIのエンドポイントに本当にリクエストを飛ばしてしまう。
Web APIの実装がまだない場合や、テストでダミーのJSONを渡したい場合はこれだと困る。

https://qiita.com/ilyaletre/items/a4c8de74a0a4f5e1e51a

Httpに限らず、特定のCmdの結果をデバッグ目的で変えたいという要求もあります。かもしれません

一年越しですが考えてみたので書いておきます

Main.elm

https://github.com/miyamoen/elm-dev-main-example

動作確認
これが元のMain.elmになります
1行目にModelをtoStringしたものを表示させています
Fetchボタンを押すとrequestが飛んでついでに乱数を発生させてModelに突っ込んでいます

実際Fetchしてくるのはfetch関数です

 fetch : Cmd Msg
 fetch =
    Task.succeed (Err Http.NetworkError)
        |> Task.perform Response

APIサーバーが用意できていないので失敗する、という体で書いています
実際はAPIを叩きに行っていると思ってください

fetchを読んでいるところは

 update : Msg -> Model -> ( Model, List (Cmd Msg) )
 update msg model =
     case msg of
         Fetch ->
             model => [ roll, fetch ]
         -- ...

Fetchはボタンを押されたときにでるMsg、rollは乱数を発生させるCmdです

ところでupdateの型がいつもと違いますね

NoRedInk/rocket-update

package
NoRedInkのrocket-updateをよく使っています
(=>), batchUpdate, batchInitが含まれています
(=>)はおなじみのタプルを作る関数です

batchUpdate, batchInitはupdateとinitをそれぞれこの型で書いて

 update : Msg -> Model -> ( Model, List (Cmd Msg) )
 init : ( Model, List (Cmd Msg) )

main関数で使うといい感じになるやつです

     Html.program
         { init = init |> batchInit
         , view = view
         , update = update >> batchUpdate
         , subscriptions = subscriptions
         }

Cmd.batch( = (!))を使わずに済ませることができます

このパッケージの使用は好みの問題なのですが、今回の問題の解決方法的にbatchUpdateを使っておくといい感じになるので使ってます

DevMain.elm

動作確認
本題です。Main.elmをスタブります(あんまりスタブの意味は分かっていません

とりあえずdev用のMainファイルを用意します。(Mainファイルを分けること自体は元記事でも言及されています
https://github.com/miyamoen/elm-dev-main-example/blob/master/DevMain.elm

DevMainでは基本的にMainのinit, update, view, subscriptonsをmainに入れているだけです
が、updateだけいじっています

 devUpdate : Msg -> Model -> ( Model, List (Cmd Msg) )
 devUpdate msg model =
     case msg of
         Fetch ->
             let
                 ( new, cmds ) =
                     update msg model

                 newCmds =
                     List.indexedMap updateFetchCmd cmds
             in
                 new => newCmds

         _ ->
             update msg model

devUpdateは通常のupdateと同様にmsgをcase式でパターンマッチしています
今回はFetchからでるCmdをスタブしたいのでFetchブランチだけ特殊な処理をし、それ以外のときはそのままMainのupdateに処理してもらっています

 ( new, cmds ) =
     update msg model

FetchブランチもまずはMain.updateに処理してもらいます
newはそのまま返します。

 newCmds =
     List.indexedMap updateFetchCmd cmds

 updateFetchCmd : Int -> Cmd Msg -> Cmd Msg
 updateFetchCmd idx cmd =
     case idx of
         0 ->
             Random.int 1 2
                 |> Random.generate Dice

         1 ->
             Ok
                 { id = "dummy id"
                 , name = "dummy name"
                 }
                 |> Task.succeed
                 |> Task.perform Response

         _ ->
             cmd

cmdsの中は[ roll, fetch]です
indexedMapでそれぞれ置き換えます

2番目のfetchをdummy userを返すCmdに書き換えました
これでどんなにFetchボタンを押そうが、requestは飛ばずにデータが返ってきます

この方法でやると、Httpに限らず他のCmdも書き換えられます。0ブランチではrollを1か2の乱数を出すCmdに書き換えています

開発中にCmdの結果をいじって挙動を確認することができるようになりましたね

なんでrocket-updateを使っているか

補足です
Cmd.batchしちゃうと分解不可なのでCmd一つ一つ置き換えるためにはrocket-updateを使っていると楽
もちろん使わなくてもCmd置き換えはできるんですが、複数同時にCmd出していると置き換えめんどうなので

まとめ

  • 元のコードはいじらない
  • Cmdは置き換えられる

今回はupdateだけでしたが他のinit, subscriptions, viewもいじいじできるし、なんだったらModelやMsgもついでに拡張してデバッグモード起動とかもできると思います(そこまでいくとコンポーネント的な話になってきますね

新しい概念を導入せず普段のElmコードの延長で書けるので特に難しいことはなかったと思います
Elm runtimeにCmd渡す前に割り込むだけです!

そもそも

APIを叩けるスタブサーバー立てたらいいのでは