こんにちは!
プログラミング未経験文系出身、Elixirの国に迷い込んだ?!見習いアルケミストのaliceと申します。
今回はElixirでTDDに基づく実装方法の手順について学んだことをまとめます。
目的
引数 a + b の実行結果を返すコードの単体テストの作成方法と実装方法の手順を理解する。
実行環境
Windows 11 + WSL2 + Ubuntu 22.04
Elixir v1.14.3
Erlang v26.0.2
Phoenix v1.7.10
前提
本シリーズは下記で生成されたPhoenixのプロジェクトを使用しています。
mix phx.new dec19
cd dec19
mix ecto.create
TDDとは?
テスト駆動開発(TDD)とは、プログラムの実装前にテストコードを書き、そのテストコードに適合するように実装とリファクタリングを進めていく方法1
TDDのメリット
例えば下記の実装をすることを考えます。
- サーバーはセンサーからInput値を受け取る。Input値は-1から1までの範囲を取る。
- サーバーはセンサーからのInput値が0以上のときに、別のPCに「センサーの値が異常値です」という通知を送信する。そうでない場合は何もしない。
このとき、TDDだと以下の手順を踏むことができます。
Input値を一旦リテラルでテストを書き、その値を受け取った以降のロジックを先に実装できる。
例:Input値をリテラルで0.5とする。
→この時は通知を送信する仕様である。なので、通知が送信されるというアウトプットでテストを書き、そのテストを通すための実装(=通知が送信されるロジック)を先に作れる。
(※実際のセンサーからInput値を受け取る部分はコメントで「TODO」等と書いておくことで後から実装するのを忘れないようにする)
defmodule Dec19 do
@doc """
Send notification to others.
## Examples
iex> send_notification(0.5)
"hogehoge"
"""
def send_notification(input) do
"hogehoge" #TODO センサーからデータ取る
end
end
とはいえ、文章で書いてもイマイチ分かりづらいので実際にTDDをやってみました。
事前準備
mix test.watchの導入
TDDではテストが通る/通らないを継続的に監視する必要があるので、まずmix test.watchを導入します。
詳しくは下記で記事化しています。
mix test.watchの動作確認(初期状態)
Running tests...
Compiling 1 file (.ex)
.....
Finished in 0.1 seconds (0.04s async, 0.07s sync)
5 tests, 0 failures
Phoenixプロジェクトの初期状態です。
画面だと下記の状態です。
mix test.watchの動作確認(わざとテストを落としてみる)
今回動作確認用に着目するテストはこちら↓
defmodule Dec19Web.PageControllerTest do
use Dec19Web.ConnCase
test "GET /", %{conn: conn} do
conn = get(conn, ~p"/")
assert html_response(conn, 200) =~ "Peace of mind from prototype to production"
end
end
実装側の下記を編集してテストをわざと落としてみます↓
#前略
<p class="text-[2rem] mt-4 font-semibold leading-10 tracking-tighter text-zinc-900">
- Peace of mind from prototype to production.
+ Peace of mind from prototype to hoge.
</p>
#後略
この時、mix test.watchの結果は下記の通りです。
Running tests...
Compiling 1 file (.ex)
....
1) test GET / (Dec19Web.PageControllerTest)
test/dec19_web/controllers/page_controller_test.exs:4
Assertion with =~ failed
code: assert html_response(conn, 200) =~ "Peace of mind from prototype to production"
left: "<!DOCTYPE html>\n
#---長いので中略---
Peace of mind from prototype to hoge.\n
#---長いので後略---
"
right: "Peace of mind from prototype to production"
stacktrace:
test/dec19_web/controllers/page_controller_test.exs:6: (test)
Finished in 0.1 seconds (0.03s async, 0.07s sync)
5 tests, 1 failure
期待値通りのテストが落ちましたのでmix test.watchの動作確認はOKです。
動作確認が済んだので、実装側の編集部分は元に戻しておきます。
#前略
<p class="text-[2rem] mt-4 font-semibold leading-10 tracking-tighter text-zinc-900">
- Peace of mind from prototype to hoge.
+ Peace of mind from prototype to production.
</p>
#後略
Running tests...
Compiling 1 file (.ex)
.....
Finished in 0.1 seconds (0.04s async, 0.06s sync)
5 tests, 0 failures
TDDをやってみた
実装側とテスト側のモジュール作成
まず実装側のモジュールと、テスト側のモジュールをそれぞれ作成します。
実装側のモジュールは下記に未使用のモジュールがあったので、今回はそちらを使いまわしました。
defmodule Dec19 do
#ここにdoctestと実装を書いていきます
end
テスト側のモジュールは新しく作成しました。
今回はdoctest2を使用しますので、下記の通り記載します。
use ExUnit.Case
doctest Dec19 #実装側のモジュールでdoctestが使用できるようになります
テストを書く
まず、テストを書きます。
defmodule Dec19 do
@doc """
Get the result of adding.
## Examples
iex> Dec19.sum(1, 2)
3
"""
end
(適当な実装を書いて)テストが落ちることを確認する
次に、引数 a + b の実行結果を返す実装を適当に書きます。
defmodule Dec19 do
@doc """
Get the result of adding.
## Examples
iex> Dec19.sum(1, 2)
3
"""
+ def sum(a, b), do: nil
end
この時点では「テストが落ちていること」(=Redの状態であること)を確認します。
Running tests...
Compiling 1 file (.ex)
.....
1) doctest Dec19.sum/2 (1) (Dec19Test)
test/dec19_test.exs:3
Doctest failed
doctest:
iex> Dec19.sum(1, 2)
3
code: Dec19.sum(1, 2) === 3
left: nil
right: 3
stacktrace:
lib/dec19.ex:6: Dec19 (module)
Finished in 0.08 seconds (0.03s async, 0.05s sync)
5 tests, 1 doctest, 1 failure
落ちたテストを通す最低限の実装をする
最後に、落ちたテストを通す最低限の実装をします。
defmodule Dec19 do
@doc """
Get the result of adding.
## Examples
iex> Dec19.sum(1, 2)
3
"""
- def sum(a, b), do: nil
+ def sum(a, b), do: 3
end
この時点では「テストが通ること」(=Greenの状態であること)を確認します。
当たり前ですがテストを書き換えるのはダメです。
Running tests...
Compiling 1 file (.ex)
......
Finished in 0.08 seconds (0.03s async, 0.05s sync)
5 tests, 1 doctest, 0 failures
以上がTDDの1イテレーションです。
リファクタリングをやってみた
リファクタリングとはTDDの2イテレーション以降を指します。
2つ以上のテストを同時に通す実装を作ることで、重複するソースコードを作らないようにしきれいに整える役割があります。
テストを書く
doctestを増やしてみます。
defmodule Dec19 do
@doc """
Get the result of adding.
## Examples
iex> Dec19.sum(1, 2)
3
+ iex> Dec19.sum(2, 3)
+ 5
"""
def sum(a, b), do: 3
end
テストが落ちることを確認する
再びRedの状態であることを確認します。
Running tests...
Compiling 1 file (.ex)
.......
1) doctest Dec19.sum/2 (2) (Dec19Test)
test/dec19_test.exs:3
Doctest failed
doctest:
iex> Dec19.sum(2, 3)
5
code: Dec19.sum(2, 3) === 5
left: 3
right: 5
stacktrace:
lib/dec19.ex:9: Dec19 (module)
Finished in 0.08 seconds (0.03s async, 0.05s sync)
5 tests, 2 doctests, 1 failure
落ちたテストを通す最低限の実装をする
落ちたテストを通す最低限の実装をします。
このとき、他のテストが落ちないように初めてa + b
という変数を用いた実装になります。
defmodule Dec19 do
@doc """
Get the result of adding.
## Examples
iex> Dec19.sum(1, 2)
3
iex> Dec19.sum(2, 3)
5
"""
- def sum(a, b), do: 3
+ def sum(a, b), do: a + b
end
再び、Greenの状態であることを確認します。
Running tests...
Compiling 1 file (.ex)
.......
Finished in 0.08 seconds (0.03s async, 0.05s sync)
5 tests, 2 doctests, 0 failures
1つの実装で2つのテストを通すリファクタリングができました(^▽^)/
まとめ
TDDにおける大事なこととして下記を学びました。
- 実装を書く前にまずテストを書いてから、一度テストが落ちること(=Redの状態であること)を確認する。
- その後落ちたテストを通す最低限の実装を行い、テストが通ること(=Greenの状態であること)を確認する。
- Red → Greenに状態が変わるたびにgit commitしておくぐらいの細かさでよい。
不明点
・実装側のモジュールの置き場所によって、テスト側のモジュールの置き場所が決まるルールがあるかもしれないが分かっていない。
~Elixirの国のご案内~
↓Elixirって何ぞや?と思ったらこちらもどぞ。Elixirは先端のアレコレをだいたい全部できちゃいます
↓ゼロからElixirを始めるなら「エリクサーチ」がおすすめ!私もエンジニア未経験から学習中です。
↓We Are The Alchemists, my friends!3
Elixirコミュニティは本当に優しくて温かい人たちばかり!
私が挫折せずにいられるのもこの恵まれた環境のおかげです。
まずは気軽にコミュニティを訪れてみてください。4
-
TDDとは何かについて。参考にさせていただきました。
https://www.qbook.jp/column/713.html
https://service.shiftinc.jp/column/4654/ ↩ -
doctestとは
@moduledoc
と@doc
で始まる文章からテストを生成してくれる機能。https://hexdocs.pm/ex_unit/1.16.0/ExUnit.DocTest.html ↩ -
@torifukukaiouさんのAwesomeな名言をお借りしました。Elixirコミュニティを一言で表すと、これに尽きます。 ↩