25
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

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を実現でき、異なるセッションでリアルタイムに更新を反映させる仕組みも提供されています。

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

LiveViewでもテストを書きたい!

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

LiveViewには、結合テスト(Integration Test)を書くための仕組みがあります。Rubyでの Capybara やElixirでの Hound と同種のものです。

テスト対象のLiveViewアプリ

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

Kapture 2021-07-13 at 17.00.18.gif

以降、テストを書いている test/todo_phoenix_liveview_web/live/todo_live_test.exs に沿って解説していきます。

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

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

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 関数で、LiveViewのプロセスを開始します。戻り値の view を使って、LiveViewを操作することができます。ここはすべてのテストケースで共通です。

また、has_element?/3 関数で、指定した要素内にテキストが含まれることをテストできます。

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

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

ことをテストします。

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 関数でフォームに値を入力し、render_submit/3 関数でSubmitできます。作成したばかりのタスクは未完了のため、1つ目の ul タグ内に表示されることをテストできました。

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

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

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 関数で取得した要素を、render_click/2 関数でクリックできます。完了済みにしたタスクは1つ目の ul タグ内には表示されず、2つ目の ul タグ内に表示されることをテストできました。

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

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

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ボタンをクリックすると編集モード(編集フォームが表示される)になり、
  2. 異なるタスクの名前を入力してSubmitすると、
  3. 編集モードが解除(元の表示に戻る)されてタスクの名前が更新されている

ことをテストします。

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ボタンをクリックすると、タスクが削除されることをテストします。

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:

25
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?