TDD
Elixir
ElixirDay 3

Elixir | テスト駆動 Elixir ( Test Driven Elixir ) #elixir #tdd

More than 1 year has passed since last update.

テスト駆動 Elixir ( Test Driven Elixir ) #elixir #tdd

概要

Elixir でテスト駆動開発をします

仕様

FizzBuzzプログラムを作成します

  • インターフェースは Fizzbuzz.fizzbuzz(from, to)
  • from, to ともに Integer のみ許容
    • Integer 以外を指定した場合は、RuntimeError を投げる
  • from から to までの FizzBuzz の結果を配列として返却
  • 15 の倍数は "FizzBuzz"
  • 3 の倍数は "Fizz"
  • 5 の倍数は "Buzz"
  • 3/5/15 の倍数以外は 入力数値をそのまま返却

手順

プロジェクトの作成

mix コマンドでプロジェクトテンプレートを生成します。
今回は

  • lib/fizzbuzz.ex
  • test/fizzbuzz_test.ex

のみを編集します。

$ mix new fizzbuzz
* creating README.md
* creating .gitignore
* creating mix.exs
* creating config
* creating config/config.exs
* creating lib
* creating lib/fizzbuzz.ex
* creating test
* creating test/test_helper.exs
* creating test/fizzbuzz_test.exs

テストを実行

自動生成直後は、空のプロダクトコードと成功するテストケースが実装されているため、
1件テストをパスします。

$ mix test
Compiled lib/fizzbuzz.ex
Generated fizzbuzz.app
.

Finished in 0.04 seconds (0.04s on load, 0.00s on tests)
1 tests, 0 failures

テストコード:数値をそのまま返却するケースを追加

  • test/fizzbuzz_test.exs
defmodule FizzbuzzTest do
  use ExUnit.Case

  test "only other numbers" do
    assert Fizzbuzz.fizzbuzz(1, 2) ==  [1, 2]
  end
end
  • テストを実行 未実装のためテストに失敗します
$ mix test --trace

FizzbuzzTest
  * only other numbers (7.0ms)

  1) test only other numbers (FizzbuzzTest)
     test/fizzbuzz_test.exs:4
     ** (UndefinedFunctionError) undefined function: Fizzbuzz.fizzbuzz/2
     stacktrace:
       (fizzbuzz) Fizzbuzz.fizzbuzz(1, 2)
       test/fizzbuzz_test.exs:5



Finished in 0.03 seconds (0.03s on load, 0.00s on tests)
1 tests, 1 failures

Randomized with seed 708312

プロダクトコード:数値をそのまま返却するケースを実装

  • lib/fizzbuzz.ex
defmodule Fizzbuzz do
  def fizzbuzz(from, to) do
    from..to |> Enum.map(&(&1))
  end
end
  • テストを実行 テストをパスしました
$ mix test --trace
Compiled lib/fizzbuzz.ex
Generated fizzbuzz.app

FizzbuzzTest
  * only other numbers (8.8ms)


Finished in 0.04 seconds (0.03s on load, 0.01s on tests)
1 tests, 0 failures

Randomized with seed 229617

テストコード:Fizz のケースを追加

  • test/fizzbuzz_test.exs
defmodule FizzbuzzTest do
  use ExUnit.Case

  test "only other numbers" do
    assert Fizzbuzz.fizzbuzz(1, 2) ==  [1, 2]
  end

  test "only Fizz" do
    assert Fizzbuzz.fizzbuzz(3, 3) ==  ["Fizz"]
    assert Fizzbuzz.fizzbuzz(6, 6) ==  ["Fizz"]
  end
end
  • テストを実行 未実装のためテストに失敗します
$ mix test --trace

FizzbuzzTest
  * only other numbers (7.5ms)
  * only Fizz (5.6ms)

  1) test only Fizz (FizzbuzzTest)
     test/fizzbuzz_test.exs:8
     Assertion with == failed
     code: Fizzbuzz.fizzbuzz(3, 3) == ["Fizz"]
     lhs:  [3]
     rhs:  ["Fizz"]
     stacktrace:
       test/fizzbuzz_test.exs:9



Finished in 0.05 seconds (0.04s on load, 0.01s on tests)
2 tests, 1 failures

プロダクトコード:Fizz を実装

  • lib/fizzbuzz.ex
defmodule Fizzbuzz do
  def fizzbuzz(from, to) do
    from..to |> Enum.map(&(fizzbuzz/1))
  end

  defp fizzbuzz(num) when rem(num, 3) == 0  do
    "Fizz"
  end

  defp fizzbuzz(num), do: num
end
  • テストを実行 テストをパスしました
$ mix test --trace
Compiled lib/fizzbuzz.ex
Generated fizzbuzz.app

FizzbuzzTest
  * only other numbers (8.3ms)
  * only Fizz (1.4ms)


Finished in 0.05 seconds (0.04s on load, 0.01s on tests)
2 tests, 0 failures

Randomized with seed 309998

テストコード:Buzz のケースを追加

  • test/fizzbuzz_test.exs
defmodule FizzbuzzTest do
  use ExUnit.Case

  test "only other numbers" do
    assert Fizzbuzz.fizzbuzz(1, 2) ==  [1, 2]
  end

  test "only Fizz" do
    assert Fizzbuzz.fizzbuzz(3, 3) ==  ["Fizz"]
    assert Fizzbuzz.fizzbuzz(6, 6) ==  ["Fizz"]
  end

  test "only Buzz" do
    assert Fizzbuzz.fizzbuzz(5, 5) ==  ["Buzz"]
    assert Fizzbuzz.fizzbuzz(10, 10) ==  ["Buzz"]
  end
end
  • テストを実行 未実装のためテストに失敗します
$ mix test --trace

FizzbuzzTest
  * only Buzz (12.4ms)

  1) test only Buzz (FizzbuzzTest)
     test/fizzbuzz_test.exs:13
     Assertion with == failed
     code: Fizzbuzz.fizzbuzz(5, 5) == ["Buzz"]
     lhs:  [5]
     rhs:  ["Buzz"]
     stacktrace:
       test/fizzbuzz_test.exs:14

  * only Fizz (0.06ms)
  * only other numbers (0.2ms)


Finished in 0.05 seconds (0.04s on load, 0.01s on tests)
3 tests, 1 failures

Randomized with seed 643604

プロダクトコード:Buzz を実装

  • lib/fizzbuzz.ex
defmodule Fizzbuzz do
  def fizzbuzz(from, to) do
    from..to |> Enum.map(&(fizzbuzz/1))
  end

  defp fizzbuzz(num) when rem(num, 5) == 0  do
    "Buzz"
  end

  defp fizzbuzz(num) when rem(num, 3) == 0  do
    "Fizz"
  end

  defp fizzbuzz(num), do: num
end
  • テストを実行 テストをパスしました
$ mix test --trace
Compiled lib/fizzbuzz.ex
Generated fizzbuzz.app

FizzbuzzTest
  * only Fizz (9.4ms)
  * only Buzz (0.04ms)
  * only other numbers (0.02ms)


Finished in 0.05 seconds (0.04s on load, 0.01s on tests)
3 tests, 0 failures

Randomized with seed 850025

テストコード:FizzBuzz のケースを追加

  • test/fizzbuzz_test.exs
defmodule FizzbuzzTest do
  use ExUnit.Case

  test "only other numbers" do
    assert Fizzbuzz.fizzbuzz(1, 2) ==  [1, 2]
  end

  test "only Fizz" do
    assert Fizzbuzz.fizzbuzz(3, 3) ==  ["Fizz"]
    assert Fizzbuzz.fizzbuzz(6, 6) ==  ["Fizz"]
  end

  test "only Buzz" do
    assert Fizzbuzz.fizzbuzz(5, 5) ==  ["Buzz"]
    assert Fizzbuzz.fizzbuzz(5, 5) ==  ["Buzz"]
  end

  test "only FizzBuzz" do
    assert Fizzbuzz.fizzbuzz(15, 15) ==  ["FizzBuzz"]
    assert Fizzbuzz.fizzbuzz(30, 30) ==  ["FizzBuzz"]
  end
end
  • テストを実行 未実装のためテストに失敗します
$ mix test --trace

FizzbuzzTest
  * only Fizz (9.5ms)
  * only Buzz (0.04ms)
  * only FizzBuzz (3.0ms)

  1) test only FizzBuzz (FizzbuzzTest)
     test/fizzbuzz_test.exs:18
     Assertion with == failed
     code: Fizzbuzz.fizzbuzz(15, 15) == ["FizzBuzz"]
     lhs:  ["Buzz"]
     rhs:  ["FizzBuzz"]
     stacktrace:
       test/fizzbuzz_test.exs:19

  * only other numbers (0.01ms)


Finished in 0.06 seconds (0.05s on load, 0.01s on tests)
4 tests, 1 failures

Randomized with seed 335109

プロダクトコード:FizzBuzz を実装

  • lib/fizzbuzz.ex
defmodule Fizzbuzz do
  def fizzbuzz(from, to) do
    from..to |> Enum.map(&(fizzbuzz/1))
  end

  defp fizzbuzz(num) when rem(num, 15) == 0 do
    "FizzBuzz"
  end

  defp fizzbuzz(num) when rem(num, 5) == 0 do
    "Buzz"
  end

  defp fizzbuzz(num) when rem(num, 3) == 0 do
    "Fizz"
  end

  defp fizzbuzz(num), do: num
end
  • テストを実行 テストをパスしました
$ mix test --trace
Compiled lib/fizzbuzz.ex
Generated fizzbuzz.app

FizzbuzzTest
  * only Buzz (10.2ms)
  * only Fizz (0.05ms)
  * only other numbers (0.03ms)
  * only FizzBuzz (0.3ms)


Finished in 0.06 seconds (0.05s on load, 0.01s on tests)
4 tests, 0 failures

Randomized with seed 629014

テストコード:Fizz / Buzz / FizzBuzz / その他混在のケースを追加

  • test/fizzbuzz_test.exs
defmodule FizzbuzzTest do
  use ExUnit.Case

  test "only other numbers" do
    assert Fizzbuzz.fizzbuzz(1, 2) ==  [1, 2]
  end

  test "only Fizz" do
    assert Fizzbuzz.fizzbuzz(3, 3) ==  ["Fizz"]
    assert Fizzbuzz.fizzbuzz(6, 6) ==  ["Fizz"]
  end

  test "only Buzz" do
    assert Fizzbuzz.fizzbuzz(5, 5) ==  ["Buzz"]
    assert Fizzbuzz.fizzbuzz(5, 5) ==  ["Buzz"]
  end

  test "only FizzBuzz" do
    assert Fizzbuzz.fizzbuzz(15, 15) ==  ["FizzBuzz"]
    assert Fizzbuzz.fizzbuzz(30, 30) ==  ["FizzBuzz"]
  end

  test "mix Fizz / Buzz / FizzBuzz / Other" do
    assert Fizzbuzz.fizzbuzz(1, 30) ==  [
      1, 2, "Fizz", 4, "Buzz",
      "Fizz", 7, 8, "Fizz", "Buzz",
      11, "Fizz", 13, 14, "FizzBuzz",
      16, 17, "Fizz", 19, "Buzz",
      "Fizz", 22, 23, "Fizz", "Buzz",
      26, "Fizz", 28, 29, "FizzBuzz"
    ]
  end
end
  • テストを実行 テストをパスしました
$ mix test --trace
Compiled lib/fizzbuzz.ex
Generated fizzbuzz.app

FizzbuzzTest
  * only Buzz (9.9ms)
  * only Fizz (0.04ms)
  * only other numbers (0.03ms)
  * only FizzBuzz (0.09ms)
  * mix Fizz / Buzz / FizzBuzz / Other (0.03ms)


Finished in 0.06 seconds (0.05s on load, 0.01s on tests)
5 tests, 0 failures

Randomized with seed 572980

テストコード:from に不正な引数( Integer 以外)を指定するケースを追加

  • test/fizzbuzz_test.exs
defmodule FizzbuzzTest do
  use ExUnit.Case

  test "only other numbers" do
    assert Fizzbuzz.fizzbuzz(1, 2) ==  [1, 2]
  end

  test "only Fizz" do
    assert Fizzbuzz.fizzbuzz(3, 3) ==  ["Fizz"]
    assert Fizzbuzz.fizzbuzz(6, 6) ==  ["Fizz"]
  end

  test "only Buzz" do
    assert Fizzbuzz.fizzbuzz(5, 5) ==  ["Buzz"]
    assert Fizzbuzz.fizzbuzz(5, 5) ==  ["Buzz"]
  end

  test "only FizzBuzz" do
    assert Fizzbuzz.fizzbuzz(15, 15) ==  ["FizzBuzz"]
    assert Fizzbuzz.fizzbuzz(30, 30) ==  ["FizzBuzz"]
  end

  test "mix Fizz / Buzz / FizzBuzz / Other" do
    expected = [
      1, 2, "Fizz", 4, "Buzz",
      "Fizz", 7, 8, "Fizz", "Buzz",
      11, "Fizz", 13, 14, "FizzBuzz",
      16, 17, "Fizz", 19, "Buzz",
      "Fizz", 22, 23, "Fizz", "Buzz",
      26, "Fizz", 28, 29, "FizzBuzz"
    ]
    assert Fizzbuzz.fizzbuzz(1, 30) == expected
  end

  test "from is not integer" do
    assert_raise RuntimeError, "invalid argument", fn ->
      Fizzbuzz.fizzbuzz("1", 15)
    end
  end
end
  • テストを実行 未実装のためテストに失敗します
$ mix test --trace

FizzbuzzTest
  * mix Fizz / Buzz / FizzBuzz / Other (9.0ms)
  * only Buzz (1.2ms)
  * from is not integer (10.1ms)

  1) test from is not integer (FizzbuzzTest)
     test/fizzbuzz_test.exs:35
     Expected exception RuntimeError but got Protocol.UndefinedError (protocol Range.Iterator not implemented for "1")
     stacktrace:
       test/fizzbuzz_test.exs:36

  * only other numbers (0.03ms)
  * only Fizz (0.1ms)
  * only FizzBuzz (0.02ms)


Finished in 0.09 seconds (0.07s on load, 0.02s on tests)
6 tests, 1 failures

Randomized with seed 505334

プロダクトコード:from に不正な引数( Integer 以外)を指定した場合に、RuntimeError を投げるように実装

  • lib/fizzbuzz.ex
defmodule Fizzbuzz do
  def fizzbuzz(from, to) when is_integer(from) do
    from..to |> Enum.map(&(fizzbuzz/1))
  end

  def fizzbuzz(_from, _to) do
    raise "invalid argument"
  end

  defp fizzbuzz(num) when rem(num, 15) == 0 do
    "FizzBuzz"
  end

  defp fizzbuzz(num) when rem(num, 5) == 0 do
    "Buzz"
  end

  defp fizzbuzz(num) when rem(num, 3) == 0 do
    "Fizz"
  end

  defp fizzbuzz(num), do: num
end
  • テストを実行 テストをパスしました
$ mix test --trace
Compiled lib/fizzbuzz.ex
Generated fizzbuzz.app

FizzbuzzTest
  * only other numbers (8.5ms)
  * only FizzBuzz (1.4ms)
  * from is not integer (2.7ms)
  * only Fizz (0.07ms)
  * mix Fizz / Buzz / FizzBuzz / Other (0.02ms)
  * only Buzz (0.08ms)


Finished in 0.07 seconds (0.06s on load, 0.01s on tests)
6 tests, 0 failures

Randomized with seed 92177

テストコード:to に不正な引数( Integer 以外)を指定するケースを追加

  • test/fizzbuzz_test.exs
defmodule FizzbuzzTest do
  use ExUnit.Case

  test "only other numbers" do
    assert Fizzbuzz.fizzbuzz(1, 2) ==  [1, 2]
  end

  test "only Fizz" do
    assert Fizzbuzz.fizzbuzz(3, 3) ==  ["Fizz"]
    assert Fizzbuzz.fizzbuzz(6, 6) ==  ["Fizz"]
  end

  test "only Buzz" do
    assert Fizzbuzz.fizzbuzz(5, 5) ==  ["Buzz"]
    assert Fizzbuzz.fizzbuzz(5, 5) ==  ["Buzz"]
  end

  test "only FizzBuzz" do
    assert Fizzbuzz.fizzbuzz(15, 15) ==  ["FizzBuzz"]
    assert Fizzbuzz.fizzbuzz(30, 30) ==  ["FizzBuzz"]
  end

  test "mix Fizz / Buzz / FizzBuzz / Other" do
    expected = [
      1, 2, "Fizz", 4, "Buzz",
      "Fizz", 7, 8, "Fizz", "Buzz",
      11, "Fizz", 13, 14, "FizzBuzz",
      16, 17, "Fizz", 19, "Buzz",
      "Fizz", 22, 23, "Fizz", "Buzz",
      26, "Fizz", 28, 29, "FizzBuzz"
    ]
    assert Fizzbuzz.fizzbuzz(1, 30) == expected
  end

  test "from is not integer" do
    assert_raise RuntimeError, "invalid argument", fn ->
      Fizzbuzz.fizzbuzz("1", 15)
    end
  end

  test "to is not integer" do
    assert_raise RuntimeError, "invalid argument", fn ->
      Fizzbuzz.fizzbuzz(1, "15")
    end
  end
end
  • テストを実行 未実装のためテストに失敗します
$ mix test --trace

FizzbuzzTest
  * only other numbers (8.0ms)
  * from is not integer (5.2ms)
  * only FizzBuzz (0.03ms)
  * only Fizz (0.04ms)
  * only Buzz (0.04ms)
  * mix Fizz / Buzz / FizzBuzz / Other (0.02ms)
  * to is not integer (4.9ms)

  1) test to is not integer (FizzbuzzTest)
     test/fizzbuzz_test.exs:41
     Expected exception RuntimeError but got FunctionClauseError (no function clause matching in Range.Iterator.Integer.next/2)
     stacktrace:
       test/fizzbuzz_test.exs:42



Finished in 0.09 seconds (0.07s on load, 0.02s on tests)
7 tests, 1 failures

Randomized with seed 685563

プロダクトコード:to に不正な引数( Integer 以外)を指定した場合に、RuntimeError を投げるように実装

  • lib/fizzbuzz.ex
defmodule Fizzbuzz do
  def fizzbuzz(from, to) when is_integer(from) do
    from..to |> Enum.map(&(fizzbuzz/1))
  end

  def fizzbuzz(_from, _to) do
    raise "invalid argument"
  end

  defp fizzbuzz(num) when rem(num, 15) == 0 do
    "FizzBuzz"
  end

  defp fizzbuzz(num) when rem(num, 5) == 0 do
    "Buzz"
  end

  defp fizzbuzz(num) when rem(num, 3) == 0 do
    "Fizz"
  end

  defp fizzbuzz(num), do: num
end
  • テストを実行 テストをパスしました
$ mix test --trace
Compiled lib/fizzbuzz.ex
Generated fizzbuzz.app

FizzbuzzTest
  * to is not integer (9.2ms)
  * from is not integer (0.06ms)
  * only other numbers (4.8ms)
  * mix Fizz / Buzz / FizzBuzz / Other (0.03ms)
  * only FizzBuzz (0.2ms)
  * only Buzz (0.02ms)
  * only Fizz (0.08ms)


Finished in 0.09 seconds (0.08s on load, 0.01s on tests)
7 tests, 0 failures

Randomized with seed 201398

プロダクトコード:リファクタリング

各プライベート関数の処理が短いので1行で記述するように修正します

  • lib/fizzbuzz.ex
defmodule Fizzbuzz do
  def fizzbuzz(from, to) when is_integer(from) and is_integer(to) do
    from..to |> Enum.map(&(fizzbuzz/1))
  end

  def fizzbuzz(_from, _to), do: raise "invalid argument"
  defp fizzbuzz(num) when rem(num, 15) == 0, do: "FizzBuzz"
  defp fizzbuzz(num) when rem(num, 5) == 0, do: "Buzz"
  defp fizzbuzz(num) when rem(num, 3) == 0, do: "Fizz"
  defp fizzbuzz(num), do: num
end
  • テストを実行 テストをパスしました。 テストを壊さずにリファクタリングに成功しました。
$ mix test --trace
Compiled lib/fizzbuzz.ex
Generated fizzbuzz.app

FizzbuzzTest
  * to is not integer (7.5ms)
  * only Buzz (4.1ms)
  * only Fizz (0.2ms)
  * from is not integer (0.1ms)
  * only FizzBuzz (0.2ms)
  * only other numbers (0.02ms)
  * mix Fizz / Buzz / FizzBuzz / Other (0.09ms)


Finished in 0.07 seconds (0.06s on load, 0.01s on tests)
7 tests, 0 failures

Randomized with seed 797885

完成したプロジェクトを実行

$ mix run -e 'IO.inspect Fizzbuzz.fizzbuzz(1, 30)'
[1, 2, "Fizz", 4, "Buzz", "Fizz", 7, 8, "Fizz", "Buzz", 11, "Fizz", 13, 14,
 "FizzBuzz", 16, 17, "Fizz", 19, "Buzz", "Fizz", 22, 23, "Fizz", "Buzz", 26,
 "Fizz", 28, 29, "FizzBuzz"]

$ mix run -e 'IO.inspect Fizzbuzz.fizzbuzz("1", 30)'
** (RuntimeError) invalid argument
    (fizzbuzz) lib/fizzbuzz.ex:6: Fizzbuzz.fizzbuzz/2
    (stdlib) erl_eval.erl:657: :erl_eval.do_apply/6
    (stdlib) erl_eval.erl:865: :erl_eval.expr_list/6
    (stdlib) erl_eval.erl:407: :erl_eval.expr/5
    (elixir) src/elixir.erl:175: :elixir.erl_eval/3
    (elixir) src/elixir.erl:163: :elixir.eval_forms/4
    (elixir) lib/code.ex:140: Code.eval_string/3
    (elixir) lib/enum.ex:537: Enum."-each/2-lists^foreach/1-0-"/2

$ mix run -e 'IO.inspect Fizzbuzz.fizzbuzz(1, "30")'
** (RuntimeError) invalid argument
    (fizzbuzz) lib/fizzbuzz.ex:6: Fizzbuzz.fizzbuzz/2
    (stdlib) erl_eval.erl:657: :erl_eval.do_apply/6
    (stdlib) erl_eval.erl:865: :erl_eval.expr_list/6
    (stdlib) erl_eval.erl:407: :erl_eval.expr/5
    (elixir) src/elixir.erl:175: :elixir.erl_eval/3
    (elixir) src/elixir.erl:163: :elixir.eval_forms/4
    (elixir) lib/code.ex:140: Code.eval_string/3
    (elixir) lib/enum.ex:537: Enum."-each/2-lists^foreach/1-0-"/2

$ mix run -e 'IO.inspect Fizzbuzz.fizzbuzz(15, 1)'
["FizzBuzz", 14, 13, "Fizz", 11, "Buzz", "Fizz", 8, 7, "Fizz", "Buzz", 4, "Fizz", 2, 1]

Complete!