LoginSignup
26
23

More than 5 years have passed since last update.

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

Posted at

テスト駆動 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!

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