LoginSignup
13
1

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

Last updated at Posted at 2023-12-24

こんにちは!
プログラミング未経験文系出身、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
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」等と書いておくことで後から実装するのを忘れないようにする)

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を導入します。
詳しくは下記で記事化しています。

mix test.watchの動作確認(初期状態)

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

mix test.watchの動作確認(わざとテストを落としてみる)

今回動作確認用に着目するテストはこちら↓

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

実装側の下記を編集してテストをわざと落としてみます↓

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

この時、mix test.watchの結果は下記の通りです。

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です。
動作確認が済んだので、実装側の編集部分は元に戻しておきます。

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
Running tests...
Compiling 1 file (.ex)
.....
Finished in 0.1 seconds (0.04s async, 0.06s sync)
5 tests, 0 failures

TDDをやってみた

実装側とテスト側のモジュール作成

まず実装側のモジュールと、テスト側のモジュールをそれぞれ作成します。

実装側のモジュールは下記に未使用のモジュールがあったので、今回はそちらを使いまわしました。

lib/dec19.ex
defmodule Dec19 do
  #ここにdoctestと実装を書いていきます
end

テスト側のモジュールは新しく作成しました。
今回はdoctest2を使用しますので、下記の通り記載します。

test/dec19_test.exs
  use ExUnit.Case
  doctest Dec19 #実装側のモジュールでdoctestが使用できるようになります

テストを書く

まず、テストを書きます。

lib/dec19.ex
 defmodule Dec19 do
   @doc """
   Get the result of adding.

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

(適当な実装を書いて)テストが落ちることを確認する

次に、引数 a + b の実行結果を返す実装を適当に書きます。

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
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

落ちたテストを通す最低限の実装をする

最後に、落ちたテストを通す最低限の実装をします。

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
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を増やしてみます。

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
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という変数を用いた実装になります。

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
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:

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

We Are The Alchemists, my friends!:bouquet:3
Elixirコミュニティは本当に優しくて温かい人たちばかり!
私が挫折せずにいられるのもこの恵まれた環境のおかげです。
まずは気軽にコミュニティを訪れてみてください。4

  1. TDDとは何かについて。参考にさせていただきました。
    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

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

  4. @kn339264さんの素敵なスライドをお借りしました。Elixirコミュニティはいろんな形で活動中!

13
1
1

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
13
1