ElixirでTDD①- doctestでTDDの基本サイクルを学ぶ

こんにちは！
プログラミング未経験文系出身、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のプロジェクトを使用しています。
```bash:bash
mix phx.new dec19
cd dec19
mix ecto.create
```

# TDDとは？
テスト駆動開発（TDD）とは、プログラムの実装前にテストコードを書き、そのテストコードに適合するように実装とリファクタリングを進めていく方法[^1]

# TDDのメリット
例えば下記の実装をすることを考えます。
- サーバーはセンサーからInput値を受け取る。Input値は-1から1までの範囲を取る。
- サーバーはセンサーからのInput値が0以上のときに、別のPCに「センサーの値が異常値です」という通知を送信する。そうでない場合は何もしない。

このとき、TDDだと以下の手順を踏むことができます。

:::note info
Input値を一旦リテラルでテストを書き、その値を受け取った以降のロジックを先に実装できる。

例：Input値をリテラルで0.5とする。
→この時は通知を送信する仕様である。なので、通知が送信されるというアウトプットでテストを書き、そのテストを通すための実装(=通知が送信されるロジック)を先に作れる。

(※実際のセンサーからInput値を受け取る部分はコメントで「TODO」等と書いておくことで後から実装するのを忘れないようにする)
:::

```diff_elixir:lib/dec19.ex
 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を導入します。
詳しくは下記で記事化しています。

https://qiita.com/Alicesky2127/items/8aaec05b836673eab767

### mix test.watchの動作確認(初期状態)
```bash:bash
Running tests...
Compiling 1 file (.ex)
.....
Finished in 0.1 seconds (0.04s async, 0.07s sync)
5 tests, 0 failures
```

Phoenixプロジェクトの初期状態です。
画面だと下記の状態です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/677280/b85bf03b-7384-4688-ac3a-51e48f7e887b.png)


### mix test.watchの動作確認(わざとテストを落としてみる)
今回動作確認用に着目するテストはこちら↓
```diff_elixir:test/dec19_web/controllers/page_controller_test.exs
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
```

実装側の下記を編集してテストをわざと落としてみます↓
```diff_elixir:lib/dec19_web/controllers/page_html/home.html.heex
#前略
     <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>
#後略
```
画面だと下記の状態です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/677280/692b3a45-5b94-6581-7a35-73a34ce39cd8.png)

この時、mix test.watchの結果は下記の通りです。
```bash:bash
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です。
動作確認が済んだので、実装側の編集部分は元に戻しておきます。
```diff_elixir:lib/dec19_web/controllers/page_html/home.html.heex
#前略
     <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>
#後略
```
```bash:bash
Running tests...
Compiling 1 file (.ex)
.....
Finished in 0.1 seconds (0.04s async, 0.06s sync)
5 tests, 0 failures
```

# TDDをやってみた
## 実装側とテスト側のモジュール作成
まず実装側のモジュールと、テスト側のモジュールをそれぞれ作成します。

実装側のモジュールは下記に未使用のモジュールがあったので、今回はそちらを使いまわしました。
```diff_elixir:lib/dec19.ex
defmodule Dec19 do
  #ここにdoctestと実装を書いていきます
end
```

テスト側のモジュールは新しく作成しました。
今回はdoctest[^2]を使用しますので、下記の通り記載します。
```diff_elixir:test/dec19_test.exs
  use ExUnit.Case
  doctest Dec19 #実装側のモジュールでdoctestが使用できるようになります
```

## テストを書く
まず、テストを書きます。
```diff_elixir:lib/dec19.ex
 defmodule Dec19 do
   @doc """
   Get the result of adding.

   ## Examples
       iex> Dec19.sum(1, 2)
       3
   """
 end
```

## (適当な実装を書いて)テストが落ちることを確認する
次に、引数 a + b の実行結果を返す実装を適当に書きます。
```diff_elixir:lib/dec19.ex
 defmodule Dec19 do
   @doc """
   Get the result of adding.

   ## Examples
       iex> Dec19.sum(1, 2)
       3
   """
+    def sum(a, b), do: nil
end
```
この時点では「テストが落ちていること」(=Redの状態であること)を確認します。
```bash:bash
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
```
## 落ちたテストを通す最低限の実装をする
最後に、落ちたテストを通す最低限の実装をします。
```diff_elixir:lib/dec19.ex
 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の状態であること)を確認します。
当たり前ですがテストを書き換えるのはダメです。
```bash:bash
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を増やしてみます。
```diff_elixir:lib/dec19.ex
 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の状態であることを確認します。
```bash:bash
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`という変数を用いた実装になります。
```diff_elixir:lib/dec19.ex
 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の状態であることを確認します。
```bash:bash
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は先端のアレコレをだいたい全部できちゃいます:laughing::sparkles::sparkles:

https://qiita.com/piacerex/items/09876caa1e17169ec5e1

↓ゼロからElixirを始めるなら「エリクサーチ」がおすすめ！私もエンジニア未経験から学習中です。

https://elixir-lang.info/topics/entry_column


↓[We Are The Alchemists, my friends!:bouquet:](https://qiita.com/torifukukaiou/items/6bcd5bbb80df9e594e9e "Awesome-san's Awesome article")[^10]
Elixirコミュニティは本当に優しくて温かい人たちばかり！
私が挫折せずにいられるのもこの恵まれた環境のおかげです。
まずは気軽にコミュニティを訪れてみてください。[^11]


https://speakerdeck.com/elijo/elixirkomiyunitei-falsebu-kifang-guo-nei-onrainbian


[^1]: TDDとは何かについて。参考にさせていただきました。<br>https://www.qbook.jp/column/713.html
https://service.shiftinc.jp/column/4654/

[^2]:doctestとは`@moduledoc`と `@doc`で始まる文章からテストを生成してくれる機能。https://hexdocs.pm/ex_unit/1.16.0/ExUnit.DocTest.html

[^10]: @torifukukaiouさんのAwesomeな名言をお借りしました。Elixirコミュニティを一言で表すと、これに尽きます。

[^11]: @kn339264さんの素敵なスライドをお借りしました。Elixirコミュニティはいろんな形で活動中！