[Elixir] Phoenix LiveViewのテストを書く

# Phoenix LiveViewをご存じない方へ

Webアプリを開発するにあたって、リッチなUIを実現するためにReactやVue.jsでSPAを実装し、APIをGo等で実装するというように、フロントエンドとバックエンドを分離して開発するケースが増えました。

フロントエンドエンジニアとサーバサイドエンジニアがそれぞれ複数人いる状況であればメリットが大きいですが、私の観測範囲内だとエンジニアは足りないことが多いですし、

- フロントエンドとバックエンドの両方を高いクオリティで対応できるエンジニアは稀で、分業化が進みがち
- ある機能を実装する際に、バックエンド側を対応しないとフロントエンド側が進められず、ボトルネックが発生する（逆に、バックエンド側の対応は終わっているのに、フロントエンド側のタスクが詰まっていて着手できないケースも）
- 単なるHTML/CSSで十分な画面やちょっとしたJSを書けば済むような画面を実装する場合でも、ReactやVue.jsの作法で実装する必要がある

といったデメリットもあります。

そんな中、Elixir/Phoenixの __LiveView__ という技術が誕生しました。LiveViewでは以下の動画（Twitterクローンのライブコーディング）のように、APIやJSを書かずElixirだけでリッチなUIを実現でき、異なるセッションでリアルタイムに更新を反映させる仕組みも提供されています。

<iframe width="560" height="315" src="https://www.youtube.com/embed/MZvmYaFkNJI" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

もちろんメリットばかりではありませんが、他の言語でもLaravelのLivewireやRailsのHotwireなど似たものがあり、非常に注目されている技術だと思います。詳しい仕組みや実装方法については、公式ドキュメントや @piacerex さんの記事を見るのがおすすめです。

https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html

https://qiita.com/piacerex/items/3f8ee18c9443d63955bf

# LiveViewでもテストを書きたい！

LiveViewが素晴らしい技術だというのはお分かりいただけたと思いますが、本番環境で使うのであればテストは書いておきたいですよね。

LiveViewには、結合テスト（Integration Test）を書くための仕組みがあります。Rubyでの [Capybara](https://github.com/teamcapybara/capybara) やElixirでの [Hound](https://github.com/HashNuke/hound) と同種のものです。

https://hexdocs.pm/phoenix_live_view/Phoenix.LiveViewTest.html

# テスト対象のLiveViewアプリ

https://github.com/mokichi/todo-phoenix-liveview

よくあるToDoアプリですが、1画面内でタスクの操作を完結できるようなつくりにしています。ソースを見てもらうと分かりますが、このLiveViewアプリも自分では一切JSを書いていません。

![Kapture 2021-07-13 at 17.00.18.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28249/4e7da51e-96f2-ef09-39f4-6f7e3b3a3508.gif)

以降、テストを書いている [test/todo_phoenix_liveview_web/live/todo_live_test.exs](https://github.com/mokichi/todo-phoenix-liveview/blob/main/test/todo_phoenix_liveview_web/live/todo_live_test.exs) に沿って解説していきます。

## タスク一覧が表示されていることをテストする

1つ目の `ul` タグ内に未完了のタスクが、2つ目の `ul` タグ内に完了済みタスクが表示されることをテストします。

```elixir
test "lists all tasks", %{conn: conn, task: task, completed_task: completed_task} do
  {:ok, view, _html} = live(conn, Routes.todo_index_path(conn, :index))

  assert view |> has_element?("ul:nth-of-type(1) li", task.title)
  assert view |> has_element?("ul:nth-of-type(2) li", completed_task.title)
end
```

[live/2](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveViewTest.html#live/2) 関数で、LiveViewのプロセスを開始します。戻り値の `view` を使って、LiveViewを操作することができます。ここはすべてのテストケースで共通です。

また、[has_element?/3](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveViewTest.html#has_element?/3) 関数で、指定した要素内にテキストが含まれることをテストできます。

## タスクを作成できることをテストする

1. フォームにタスクのタイトルを入力し、
1. Submitすると、
1. 新しいタスクが追加される

ことをテストします。

```elixir
test "creates new task", %{conn: conn} do
  {:ok, view, _html} = live(conn, Routes.todo_index_path(conn, :index))

  new_task_title = "New task"
  view
  |> form("form", %{task: %{title: new_task_title}})
  |> render_submit()

  assert view |> has_element?("ul:nth-of-type(1) li", new_task_title)
end
```

[form/3](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveViewTest.html#form/3) 関数でフォームに値を入力し、[render_submit/3](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveViewTest.html#render_submit/2) 関数でSubmitできます。作成したばかりのタスクは未完了のため、1つ目の `ul` タグ内に表示されることをテストできました。

## 未完了のタスクを完了済みに切り替えられることをテストする

未完了のタスクの左側にあるチェックボックスをクリックすると、完了済みになることをテストします。

```elixir
test "completes incompleted task", %{conn: conn, task: task} do
  {:ok, view, _html} = live(conn, Routes.todo_index_path(conn, :index))

  view
  |> element("ul:nth-of-type(1) li input[type=checkbox]")
  |> render_click()

  refute view |> has_element?("ul:nth-of-type(1) li", task.title)
  assert view |> has_element?("ul:nth-of-type(2) li", task.title)
end
```

[element/3](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveViewTest.html#element/3) 関数で取得した要素を、[render_click/2](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveViewTest.html#render_click/2) 関数でクリックできます。完了済みにしたタスクは1つ目の `ul` タグ内には表示されず、2つ目の `ul` タグ内に表示されることをテストできました。

## 完了済みのタスクを未完了に切り替えられることをテストする

未完了→完了済みの逆バージョンです。

```elixir
test "incompletes completed task", %{conn: conn, completed_task: task} do
  {:ok, view, _html} = live(conn, Routes.todo_index_path(conn, :index))

  view
  |> element("ul:nth-of-type(2) li input[type=checkbox]")
  |> render_click()

  assert view |> has_element?("ul:nth-of-type(1) li", task.title)
  refute view |> has_element?("ul:nth-of-type(2) li", task.title)
end
```

## タスクを更新できることをテストする

少し複雑なUIですが、

1. タスクの右側にあるEditボタンをクリックすると編集モード（編集フォームが表示される）になり、
1. 異なるタスクの名前を入力してSubmitすると、
1. 編集モードが解除（元の表示に戻る）されてタスクの名前が更新されている

ことをテストします。

```elixir
test "updates task", %{conn: conn} do
  {:ok, view, _html} = live(conn, Routes.todo_index_path(conn, :index))

  view
  |> element("ul:nth-of-type(1) li button", "Edit")
  |> render_click()

  new_task_title = "Updated task"
  view
  |> form("ul:nth-of-type(1) li form", %{task: %{title: new_task_title}})
  |> render_submit()

  assert view |> has_element?("ul:nth-of-type(1) li button", "Edit")
  assert view |> has_element?("ul:nth-of-type(1) li", new_task_title)
end
```

今までのテストケースと比べてステップ数が多いですが、ここまで紹介してきた関数の組み合わせで複雑なUIもテストできました。

## タスクを削除できることをテストする

タスクの右側にあるDeleteボタンをクリックすると、タスクが削除されることをテストします。

```elixir
test "deletes task", %{conn: conn, task: task} do
  {:ok, view, _html} = live(conn, Routes.todo_index_path(conn, :index))

  view
  |> element("ul:nth-of-type(1) li button", "Delete")
  |> render_click()

  refute view |> has_element?("ul li", task.title)
end
```

削除したタスクが画面内に表示されていないことをテストできました。

以上で、ToDoアプリのすべての機能についてテストを書けました :tada: 

# 最後に

今回は結合テストに関する記事ということで、テストコードしか紹介していません。LiveViewの実装は紹介したリポジトリに含まれているので、ぜひ見てみてください。コンポーネント分割（ベストな形かは分かりませんが…）もしているので、参考になれば幸いです :pray: 

https://github.com/mokichi/todo-phoenix-liveview