ElmのHttp処理部をスタブする
に対するアンサーです
要約
- 元のMain.elmをスタブしたいって理由で変更入れたくない
- updateから返却されるcmdを入れ替える
問題
Elmでアプリケーション書いているとリモートのWeb APIへリクエストを飛ばす部分がでてくる。
けれどチュートリアルの章を読むとHttpパッケージの関数を直接埋め込んで埋め込んである。この流儀に従ってそのまま実装していくと、elm-reactorで動かした際に、Web APIのエンドポイントに本当にリクエストを飛ばしてしまう。
Web APIの実装がまだない場合や、テストでダミーのJSONを渡したい場合はこれだと困る。
Httpに限らず、特定のCmdの結果をデバッグ目的で変えたいという要求もあります。かもしれません
一年越しですが考えてみたので書いておきます
Main.elm
動作確認
これが元の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を叩けるスタブサーバー立てたらいいのでは