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