Elm TDDしながらチャットコメントを実装する

> この記事は[Elm+Firebaseでチャットアプリを作る](https://qiita.com/ababup1192/items/5f64a5f6f00664ee7981)のエントリーです。
> 前記事 : [Elmでビュー(チャット画面)を作る](https://qiita.com/ababup1192/items/ed3be0b0d7b2d1b42a44)
> 次記事 : [Elm TDDしながらチャットコメントの投稿を実装する](https://qiita.com/ababup1192/items/8a8d76838883ed9b09f5)

# この記事で行うこと

本稿では、[TEA (The Elm Architecture)](https://guide.elm-lang.jp/architecture/)とスナップショットテストをしながら実装を進めていく方法をお伝えします。

- [Elmで超高速 型安全スナップショットテストのススメ](https://qiita.com/ababup1192/items/a43692b6be5abb59231c)


# 実装内容

## TEAを利用したコメント反映

以下のような流れで実装をしていきます。TDDを交えながらステップバイステップで見ていきましょう。

フォームのInputイベント -> Msg発行 -> updateによるMsgのハンドリング, Modelの更新 -> Modelをviewに反映

### コメントフォームにおけるInputイベントの実装

コメントフォームにおいて、Inputイベントが起きていることをテストします。まずは前回のviewから`chatForm`と言う名前でイベントを起こしたい箇所を抽出します。Elmでは、viewは関数と配列によって構成されているに過ぎないので関数として切り出すのはとっても簡単です。

```Main.elm
-- テスト対象の関数をexportする
module Main exposing (Msg(..), chatForm)

...

type Msg
    = UpdateContent String

view : Model -> Html Msg
view model =
    div [ class "page" ]
        [ section [ class "card" ]
            [ div [ class "card-header" ]
                [ text "Elm Chat"
                ]
            , div [ class "card-body" ]
                [ div [ class "media" ]
                    [ div [ class "media-left" ]
                        [ a [ href "#", class "icon-rounded" ] [ text "S" ]
                        ]
                    , div [ class "media-body" ]
                        [ h4 [ class "media-heading" ] [ text "Suzuki Taro Date:2016/09/01" ]
                        , div [] [ text "この文章はダミーです。文字の大きさ、量、字間、行間等を確認するために入れています。この文章はダミーです。文字の大きさ、量、字間、行間等を確認するために入れています。" ]
                        ]
                    ]
                , hr [] []
                , div
                    [ class "media" ]
                    [ div [ class "media-body" ]
                        [ h4 [ class "media-heading" ] [ text "Tanaka Jiro Date:2016/09/01" ]
                        , div [] [ text "この文章はダミーです。文字の大きさ、量、字間、行間等を確認するために入れています。" ]
                        ]
                    , div
                        [ class "media-right" ]
                        [ a [ href "#", class "icon-rounded" ] [ text "T" ]
                        ]
                    ]
                ]
            ]
        , section []
            -- ここを関数化
            [ chatForm
            ]
        ]

-- Form部分を切り出し
chatForm : Html Msg
chatForm =
    form [ class "chart-form pure-form" ]
        [ div [ class "input-group" ]
            [ input [ type_ "text", class "", placeholder "Comment" ] []
            , button [ class "pure-button button-secondary" ] [ text "SNED" ]
            ]
        ]
```

期待をテストで表現します。イベントが起きたときには、後述する`update`関数でハンドリングするために`Msg`を発行します。今回の場合、chatFormのviewから対象となるタグを辿っていってイベントをシミュレートし、`UpdateContent "abc"`というMsgが発行されることを期待します。

```Tests.elm
module Tests exposing (suite)

import Expect exposing (Expectation)
import Main exposing (..)
import Test exposing (..)
import Test.Html.Event as Event
import Test.Html.Query as Query
import Test.Html.Selector as Selector


suite : Test
suite =
    describe "The Main module"
        [ describe "chatForm"
            [ test "フォームに 'abc' と入力したら UpdateContent 'abc' の Msgが発行される" <|
                \_ ->
                    chatForm
                        |> Query.fromHtml
                        |> Query.find [ Selector.tag "input" ]
                        |> Event.simulate (Event.input "abc")
                        |> Event.expect (UpdateContent "abc")
            ]
        ]
```

良い感じでREDとなりました。

<img width="922" alt="スクリーンショット 2018-12-29 22.55.41.png" src="https://qiita-image-store.s3.amazonaws.com/0/48687/0d9dcbeb-4156-02c9-0a79-d1852ddec7de.png">

GREENになるように`onInput`イベントを追記しましょう。

```Main.elm
import Html.Events exposing (onInput)

...

chatForm : Html Msg
chatForm =
    form [ class "chart-form pure-form" ]
        [ div [ class "input-group" ]
            [ input [ type_ "text", class "", placeholder "Comment", onInput UpdateContent ] []
            , button [ class "pure-button button-secondary" ] [ text "SNED" ]
            ]
        ]
```

GREENですね。

<img width="890" alt="スクリーンショット 2018-12-29 22.58.07.png" src="https://qiita-image-store.s3.amazonaws.com/0/48687/1f89da90-bed3-6b9c-730f-727282cd6c38.png">

### Modelの反映

Inputイベントがテストできたので、Modelの定義とupdate関数の定義、viewの更新をしていきましょう。

```Main.elm
type alias Model =
    { content : String }


init : () -> ( Model, Cmd Msg )
init _ =
    ( { content = "" }, Cmd.none )

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        -- イベントで受け取った値でモデルを更新
        UpdateContent c ->
            ( { model | content = c }, Cmd.none )

view : Model -> Html Msg
view model =
    div [ class "page" ]
        [ section [ class "card" ]
            [ div [ class "card-header" ]
                [ text "Elm Chat"
                ]
            , div [ class "card-body" ]
                [ div [ class "media" ]
                    [ div [ class "media-left" ]
                        [ a [ href "#", class "icon-rounded" ] [ text "S" ]
                        ]
                    , div [ class "media-body" ]
                        [ h4 [ class "media-heading" ] [ text "Suzuki Taro Date:2016/09/01" ]
                        , div [] [ text "この文章はダミーです。文字の大きさ、量、字間、行間等を確認するために入れています。この文章はダミーです。文字の大きさ、量、字間、行間等を確認するために入れています。" ]
                        ]
                    ]
                , hr [] []
                , div
                    [ class "media" ]
                    [ div [ class "media-body" ]
                        [ h4 [ class "media-heading" ] [ text "Tanaka Jiro Date:2016/09/01" ]
                        -- ここが変更点
                        , div [] [ text model.content ]
                        ]
                    , div
                        [ class "media-right" ]
                        [ a [ href "#", class "icon-rounded" ] [ text "T" ]
                        ]
                    ]
                ]
            ]
        , section []
            [ chatForm
            ]
        ]
```

毎回`model.content`とするのは面倒な為、パターンマッチで中身だけを取り出しましょう。

```Main.elm
view { content } =
...
                          , div [] [ text content ]

```

### 実行結果

これがTEAを利用したWEBアプリケーションの構築になります。最初は慣れるまで時間が掛かりますが、すべての工程がTEAに乗っかっていくので無駄な思考が要らなくなります。また、テストとコンパイラの強力さがあるため、その2つが通ってさえいれば間違いなく動き、実行時エラーが起きないことが保証されます。またElmのデバッガはとても便利で発行されたMsgとそのときのModelをトレースすることができます。

![chat.gif](https://qiita-image-store.s3.amazonaws.com/0/48687/ad6b3139-2985-8892-cec8-02a9c1b0a728.gif)


## List.map によるレコードとHtmlの変換

### CommentレコードとHtmlの変換

Modelのときと同様に`Comment`というレコードを定義します。`mediaView`関数は、Commentを受け取りチャットのコメントを表すviewを表現します。今回もTDDで実装するため中身は空にします。

```Main.elm
module Main exposing (Comment, Msg(..), chatForm, mediaView)

type alias Comment =
    { name : String, content : String }

mediaView : Comment -> Html Msg
mediaView comment =
    div [ class "media" ]
        [ div [ class "media-left" ]
            [ a [ href "#", class "icon-rounded" ] [ text "S" ]
            ]
        , div [ class "media-body" ]
            [ h4 [ class "media-heading" ] [ text " Date:2018/12/29" ]
            , div [] [ text "" ]
            ]
        ]
```

テストを書きます。`suzukiComment =
                    mediaView (Comment "Suzuki Taro" "コメントです。")`に対して、名前とコメントがHtml上に表示されていることを期待するテストを書きます。

```Tests.elm
module Tests exposing (suite)

import Expect exposing (Expectation)
import Main exposing (..)
import Test exposing (..)
import Test.Html.Event as Event
import Test.Html.Query as Query
import Test.Html.Selector as Selector


suite : Test
suite =
    describe "The Main module"
        [ describe "chatForm"
            [ test "フォームに 'abc' と入力したら UpdateContent 'abc' の Msgが発行される" <|
                \_ ->
                    chatForm
                        |> Query.fromHtml
                        |> Query.find [ Selector.tag "input" ]
                        |> Event.simulate (Event.input "abc")
                        |> Event.expect (UpdateContent "abc")
            ]
        , describe "mediaView" <|
            let
                suzukiComment =
                    mediaView (Comment "Suzuki Taro" "コメントです。")
            in
            [ test "コメントしたのは、「Suzuki Taro」だ。" <|
                \_ ->
                    suzukiComment
                        |> Query.fromHtml
                        |> Query.find [ Selector.class "media-body" ]
                        |> Query.find [ Selector.tag "h4" ]
                        |> Query.has [ Selector.text "Suzuki Taro Date:2018/12/29" ]
            , test "コメント内容は、「コメントです。」だ。" <|
                \_ ->
                    suzukiComment
                        |> Query.fromHtml
                        |> Query.find [ Selector.class "media-body" ]
                        |> Query.find [ Selector.tag "div" ]
                        |> Query.has [ Selector.text "コメントです。" ]
            ]
        ]
```

期待通りテストが失敗しました。

<img width="866" alt="スクリーンショット 2018-12-29 23.45.47.png" src="https://qiita-image-store.s3.amazonaws.com/0/48687/49a2ab48-f21f-82db-cb1e-a60cd00643d5.png">

<img width="866" alt="スクリーンショット 2018-12-29 23.45.56.png" src="https://qiita-image-store.s3.amazonaws.com/0/48687/d1b786ff-f242-10c0-a649-d137bc014237.png">

テストが通った為、TEAに組み込んでいきます。Modelにcommentsを初期値として追加しましょう。viewではテストしたmediaViewに対して複数のCommentを渡していくので、[List.map](https://package.elm-lang.org/packages/elm/core/latest/List#map)を利用しましょう。型として見ると以下のように変換されているわけですね。List同士の連結には、[(++)](https://package.elm-lang.org/packages/elm/core/latest/Basics#(++))を利用します。

```elm 
List Comment -> List (Html Msg)
```

```Main.elm
type alias Model =
    { content : String, comments : List Comment }


init : () -> ( Model, Cmd Msg )
init _ =
    ( { content = ""
      , comments =
            [ Comment "Suzuki Taro" "1つ目のコメントです。"
            , Comment "Suzuki Taro" "2つ目のコメントです。"
            , Comment "Suzuki Taro" "3つ目のコメントです。"
            ]
      }
    , Cmd.none
    )

view : Model -> Html Msg
view { content, comments } =
    div [ class "page" ]
        [ section [ class "card" ]
            [ div [ class "card-header" ]
                [ text "Elm Chat"
                ]
            -- ここが変更点。
            , div [ class "card-body" ] <|
                List.map mediaView comments
                    ++ [ hr [] []
                       , div
                            [ class "media" ]
                            [ div [ class "media-body" ]
                                [ h4 [ class "media-heading" ] [ text "Tanaka Jiro Date:2016/09/01" ]
                                , div [] [ text content ]
                                ]
                            , div
                                [ class "media-right" ]
                                [ a [ href "#", class "icon-rounded" ] [ text "T" ]
                                ]
                            ]
                       ]
            ]
        , section []
            [ chatForm
            ]
        ]
```

アプリケーションを起動すると、無事反映されていることがわかりました。

<img width="1358" alt="スクリーンショット 2018-12-29 23.58.06.png" src="https://qiita-image-store.s3.amazonaws.com/0/48687/4fb64f47-1765-0c3a-1424-e1e1cd2e0f43.png">


## ソースコード

この時点での[ソースコード](https://github.com/ababup1192/elm-firebase-chat/tree/e4317ac054444ac4a817acb8b0343b0538204630)