TDDでFizzBuzz問題をPythonで書いてみる
趣旨
現在私が携わっているプロジェクトではPythonとPytestを使って実装しています。
今回チームでTDDの勉強会をやりたいと考え、その練習のためにt-wadaさんのTDD Boot Camp 2020のライブコーディング部分をPythonでやってみました。
環境はcyber-dojoを使いました。
参考
t-wadaさんの「TDD Boot Camp 2020 Online #1 基調講演/ライブコーディング」の動画です
こちらはjavaでのコーディングですが、この動画を参考にPythonで進めていきます
同じくt-wadaさんがテスト駆動開発について説明されているスライドです
お題
ライブコーディング:前編
要件を整理
テスト駆動開発は徹頭徹尾各個撃破していく手法なので日本語を整えながらTODOリストとして整理する
TODO.txt
テスト容易性:高 重要度:高
- [] 数を文字列に変換する
- [] 3の倍数のときは数の代わりに「Fizz」に変換する
- [] 5の倍数のときは数の代わりに「Buzz」に変換する
- [] 3と5両方の倍数のときには数の代わりに「FizzBuzz」に変換する
テスト容易性:低 重要度:低
- [] 1からnまで
- [] 1から100まで
- [] プリントする
サイクル1-1:テストを実行して失敗させる
数を文字列に変換するでは具体性が足りず、テスト実施ができないため、具体的に考える
TODO.txt
テスト容易性:高 重要度:高
- [] 数を文字列に変換する
- [] 1を渡すと文字列"1"を返す
- [] 3の倍数のときは数の代わりに「Fizz」に変換する
- [] 5の倍数のときは数の代わりに「Buzz」に変換する
- [] 3と5両方の倍数のときには数の代わりに「FizzBuzz」に変換する
テスト容易性:低 重要度:低
- [] 1からnまで
- [] 1から100まで
- [] プリントする
テストコードを書く中で、convert関数が変換処理をしてくれるものと想定する
test_fizzbuzz.py
def test_1を渡したら文字列1を返す():
# 実行
result = convert(1)
# 検証
assert result == "1"
テストを実行し、テストケースが失敗していることを確認
output
=================================== FAILURES ===================================
______________________________ test_1を渡したら文字列1を返す ______________________________
def test_1を渡したら文字列1を返す():
> assert convert(1) == "1"
E NameError: name 'convert' is not defined
test_fizzbuzz.py:2: NameError
=========================== short test summary info ============================
FAILED test_fizzbuzz.py::test_1を渡したら文字列1を返す - NameError: name 'con...
============================== 1 failed in 0.07s ===============================
当然テストコードしかなく、convert関数が存在しないため、失敗していることがわかる
次にテストコードに書いたconvert関数を実装する
とりあえず空で返す
fizzbuzz.py
def convert(num):
return ""
関数を追加したのでテストコード側でimportする
test_fizzbuzz.py
from fizzbuzz import convert
def test_1を渡したら文字列1を返す():
# 実行
result = convert(1)
# 検証
assert result == "1"
テストを実行する
output
=================================== FAILURES ===================================
______________________________ test_1を渡したら文字列1を返す ______________________________
def test_1を渡したら文字列1を返す():
> assert convert(1) == "1"
E AssertionError: assert '' == '1'
E - 1
test_fizzbuzz.py:4: AssertionError
=========================== short test summary info ============================
FAILED test_fizzbuzz.py::test_1を渡したら文字列1を返す - AssertionError: asse...
============================== 1 failed in 0.06s ===============================
テストケースで期待している値と違うので失敗(Red)しているが、期待通り
問題なくテストコードが動いていることは確認できたので次に進む
サイクル1-2:テストを成功させる
テストが成功するように最小限の簡易な実装を行う
fizzbuzz.py
def convert(num):
return "1"
テストを実行し、成功することを確認
output
test_fizzbuzz.py::test_1を渡したら文字列1を返す PASSED [100%]
試しに1以外の値に修正し、テストコードが正しいことも確認しておく
fizzbuzz.py
def convert(num):
return "2"
テストを実行する
output
=================================== FAILURES ===================================
______________________________ test_1を渡したら文字列1を返す ______________________________
def test_1を渡したら文字列1を返す():
> assert convert(1) == "1"
E AssertionError: assert '2' == '1'
E - 1
E + 2
test_fizzbuzz.py:4: AssertionError
=========================== short test summary info ============================
FAILED test_fizzbuzz.py::test_1を渡したら文字列1を返す - AssertionError: asse...
============================== 1 failed in 0.07s ===============================
失敗していることが確認できたのでテストコードのテストが行えた
サイクル1-3:リファクタリングする
冗長なコードをインライン化して1行に整える
test_fizzbuzz.py
from fizzbuzz import convert
def test_1を渡したら文字列1を返す():
# 実行&検証
assert convert(1) == "1"
class定義をする
fizzbuzz.py
class FizzBuzz:
def convert(num):
return "1"
test_fizzbuzz.py
from fizzbuzz import FizzBuzz
def test_1を渡したら文字列1を返す():
assert FizzBuzz.convert(1) == "1"
テストを実行して問題ないことを確認。
テストコードもclass定義をしておく
test_fizzbuzz.py
from fizzbuzz import FizzBuzz
class TestFizzBuzz:
def test_1を渡したら文字列1を返す(self):
# 実行&検証
assert FizzBuzz.convert(1) == "1"
再度、テストを実行して問題ないことを確認
最初は設計が重たいので1週目は仮実装で進めることで、設計に集中でき、テストのテストも完了した。
実装はひどい状態なのでこの後まともな状態にしていく。
TODO
テスト容易性:高 重要度:高
- [] 数を文字列に変換する
- [x] 1を渡すと文字列"1"を返す -> 仮実装
- [] 3の倍数のときは数の代わりに「Fizz」に変換する
- [] 5の倍数のときは数の代わりに「Buzz」に変換する
- [] 3と5両方の倍数のときには数の代わりに「FizzBuzz」に変換する
テスト容易性:低 重要度:低
- [] 1からnまで
- [] 1から100まで
- [] プリントする
サイクル2-1:テストを実行して失敗させる
仮実装に対して別の数を与えることによってまともな方向に戻していくことを三角測量と呼ぶ
TODO
テスト容易性:高 重要度:高
- [] 数を文字列に変換する
- [x] 1を渡すと文字列"1"を返す -> 仮実装
- [] 2を渡すと文字列"2"を返す -> 三角測量
- [] 3の倍数のときは数の代わりに「Fizz」に変換する
- [] 5の倍数のときは数の代わりに「Buzz」に変換する
- [] 3と5両方の倍数のときには数の代わりに「FizzBuzz」に変換する
テスト容易性:低 重要度:低
- [] 1からnまで
- [] 1から100まで
- [] プリントする
テストコードにテストケースを追加する
test_fizzbuzz.py
from fizzbuzz import FizzBuzz
class TestFizzBuzz:
def test_1を渡したら文字列1を返す(self):
# 実行&検証
assert FizzBuzz.convert(1) == "1"
def test_2を渡したら文字列2を返す(self):
# 実行&検証
assert FizzBuzz.convert(2) == "2"
テストを実行し、追加したテストケースが失敗していることを確認
output
test_fizzbuzz.py::TestFizzBuzz::test_2を渡したら文字列2を返す FAILED [ 50%]
test_fizzbuzz.py::TestFizzBuzz::test_1を渡したら文字列1を返す PASSED [100%]
=================================== FAILURES ===================================
_______________________ TestFizzBuzz.test_2を渡したら文字列2を返す ________________________
self = <test_fizzbuzz.TestFizzBuzz object at 0x7fac006f5d60>
def test_2を渡したら文字列2を返す(self):
# 実行&検証
> assert FizzBuzz.convert(2) == "2"
E AssertionError: assert '1' == '2'
E - 2
E + 1
test_fizzbuzz.py:9: AssertionError
=========================== short test summary info ============================
FAILED test_fizzbuzz.py::TestFizzBuzz::test_2を渡したら文字列2を返す - Assert...
========================= 1 failed, 1 passed in 0.06s ==========================
サイクル2-2:テストを成功させる
fizzbuzz.py
class FizzBuzz:
def convert(num):
return str(num)
テストを実行し、パスすることを確認
output
test_fizzbuzz.py::TestFizzBuzz::test_2を渡したら文字列2を返す PASSED [ 50%]
test_fizzbuzz.py::TestFizzBuzz::test_1を渡したら文字列1を返す PASSED [100%]
サイクル2-3:リファクタリングする
リファクタリングする箇所はなさそうなので終了
TODO
テスト容易性:高 重要度:高
- [x] 数を文字列に変換する
- [x] 1を渡すと文字列"1"を返す -> 仮実装
- [x] 2を渡すと文字列"2"を返す -> 三角測量
- [] 3の倍数のときは数の代わりに「Fizz」に変換する
- [] 5の倍数のときは数の代わりに「Buzz」に変換する
- [] 3と5両方の倍数のときには数の代わりに「FizzBuzz」に変換する
テスト容易性:低 重要度:低
- [] 1からnまで
- [] 1から100まで
- [] プリントする
サイクル3-1:テストを実行して失敗させる
TODO
テスト容易性:高 重要度:高
- [x] 数を文字列に変換する
- [x] 1を渡すと文字列"1"を返す -> 仮実装
- [x] 2を渡すと文字列"2"を返す -> 三角測量
- [] 3の倍数のときは数の代わりに「Fizz」に変換する
- [] 3を渡すと文字列"Fizz"を返す -> 仮実装
- [] 5の倍数のときは数の代わりに「Buzz」に変換する
- [] 3と5両方の倍数のときには数の代わりに「FizzBuzz」に変換する
テスト容易性:低 重要度:低
- [] 1からnまで
- [] 1から100まで
- [] プリントする
テストコードにテストケースを追加する
test_fizzbuzz.py
from fizzbuzz import FizzBuzz
class TestFizzBuzz:
def test_1を渡したら文字列1を返す(self):
# 実行&検証
assert FizzBuzz.convert(1) == "1"
def test_2を渡したら文字列2を返す(self):
# 実行&検証
assert FizzBuzz.convert(2) == "2"
def test_3を渡したら文字列Fizzを返す(self):
# 実行&検証
assert FizzBuzz.convert(3) == "Fizz"
テストを実行し、追加したテストケースが失敗していることを確認
output
test_fizzbuzz.py::TestFizzBuzz::test_2を渡したら文字列2を返す PASSED [ 33%]
test_fizzbuzz.py::TestFizzBuzz::test_1を渡したら文字列1を返す PASSED [ 66%]
test_fizzbuzz.py::TestFizzBuzz::test_3を渡したら文字列Fizzを返す FAILED [100%]
=================================== FAILURES ===================================
______________________ TestFizzBuzz.test_3を渡したら文字列Fizzを返す ______________________
self = <test_fizzbuzz.TestFizzBuzz object at 0x7f7c7bb33400>
def test_3を渡したら文字列Fizzを返す(self):
# 実行&検証
> assert FizzBuzz.convert(3) == "Fizz"
E AssertionError: assert '3' == 'Fizz'
E - Fizz
E + 3
test_fizzbuzz.py:12: AssertionError
=========================== short test summary info ============================
FAILED test_fizzbuzz.py::TestFizzBuzz::test_3を渡したら文字列Fizzを返す - Ass...
========================= 1 failed, 2 passed in 0.08s ==========================
サイクル3-2:テストを成功させる
仮実装
fizzbuzz.py
class FizzBuzz:
def convert(num):
if num == 3:
return 'Fizz'
else:
return str(num)
テストを実行し、パスすることを確認
output
test_fizzbuzz.py::TestFizzBuzz::test_1を渡したら文字列1を返す PASSED [ 33%]
test_fizzbuzz.py::TestFizzBuzz::test_3を渡したら文字列Fizzを返す PASSED [ 66%]
test_fizzbuzz.py::TestFizzBuzz::test_2を渡したら文字列2を返す PASSED [100%]
TODO
テスト容易性:高 重要度:高
- [x] 数を文字列に変換する
- [x] 1を渡すと文字列"1"を返す -> 仮実装
- [x] 2を渡すと文字列"2"を返す -> 三角測量
- [] 3の倍数のときは数の代わりに「Fizz」に変換する
- [x] 3を渡すと文字列"Fizz"を返す -> 仮実装
- [] 5の倍数のときは数の代わりに「Buzz」に変換する
- [] 3と5両方の倍数のときには数の代わりに「FizzBuzz」に変換する
テスト容易性:低 重要度:低
- [] 1からnまで
- [] 1から100まで
- [] プリントする
サイクル3-3:リファクタリングする
今回は三角測量をせずに、このタイミングで仮実装から実装へリファクタリングで整える
fizzbuzz.py
class FizzBuzz:
def convert(num):
if num % 3 == 0:
return 'Fizz'
else:
return str(num)
テストを実行し、パスすることを確認
output
test_fizzbuzz.py::TestFizzBuzz::test_3を渡したら文字列Fizzを返す PASSED [ 33%]
test_fizzbuzz.py::TestFizzBuzz::test_2を渡したら文字列2を返す PASSED [ 66%]
test_fizzbuzz.py::TestFizzBuzz::test_1を渡したら文字列1を返す PASSED [100%]
TODO
テスト容易性:高 重要度:高
- [x] 数を文字列に変換する
- [x] 1を渡すと文字列"1"を返す -> 仮実装
- [x] 2を渡すと文字列"2"を返す -> 三角測量
- [x] 3の倍数のときは数の代わりに「Fizz」に変換する
- [x] 3を渡すと文字列"Fizz"を返す -> 仮実装 -> 実装
- [] 5の倍数のときは数の代わりに「Buzz」に変換する
- [] 3と5両方の倍数のときには数の代わりに「FizzBuzz」に変換する
テスト容易性:低 重要度:低
- [] 1からnまで
- [] 1から100まで
- [] プリントする
サイクル4-1:テストを実行して失敗させる
TODO
テスト容易性:高 重要度:高
- [x] 数を文字列に変換する
- [x] 1を渡すと文字列"1"を返す -> 仮実装
- [x] 2を渡すと文字列"2"を返す -> 三角測量
- [x] 3の倍数のときは数の代わりに「Fizz」に変換する
- [x] 3を渡すと文字列"Fizz"を返す -> 仮実装 -> 実装
- [] 5の倍数のときは数の代わりに「Buzz」に変換する
- [] 5を渡すと文字列"Buzz"を返す
- [] 3と5両方の倍数のときには数の代わりに「FizzBuzz」に変換する
テスト容易性:低 重要度:低
- [] 1からnまで
- [] 1から100まで
- [] プリントする
テストコードにテストケースを追加する
test_fizzbuzz.py
from fizzbuzz import FizzBuzz
class TestFizzBuzz:
def test_1を渡したら文字列1を返す(self):
# 実行&検証
assert FizzBuzz.convert(1) == "1"
def test_2を渡したら文字列2を返す(self):
# 実行&検証
assert FizzBuzz.convert(2) == "2"
def test_3を渡したら文字列Fizzを返す(self):
# 実行&検証
assert FizzBuzz.convert(3) == "Fizz"
def test_5を渡したら文字列Buzzを返す(self):
# 実行&検証
assert FizzBuzz.convert(5) == "Buzz"
テストを実行し、追加したテストケースが失敗していることを確認
output
test_fizzbuzz.py::TestFizzBuzz::test_3を渡したら文字列Fizzを返す PASSED [ 25%]
test_fizzbuzz.py::TestFizzBuzz::test_5を渡したら文字列Buzzを返す FAILED [ 50%]
test_fizzbuzz.py::TestFizzBuzz::test_1を渡したら文字列1を返す PASSED [ 75%]
test_fizzbuzz.py::TestFizzBuzz::test_2を渡したら文字列2を返す PASSED [100%]
=================================== FAILURES ===================================
______________________ TestFizzBuzz.test_5を渡したら文字列Buzzを返す ______________________
self = <test_fizzbuzz.TestFizzBuzz object at 0x7fba68ef06d0>
def test_5を渡したら文字列Buzzを返す(self):
# 実行&検証
> assert FizzBuzz.convert(5) == "Buzz"
E AssertionError: assert '5' == 'Buzz'
E - Buzz
E + 5
test_fizzbuzz.py:15: AssertionError
=========================== short test summary info ============================
FAILED test_fizzbuzz.py::TestFizzBuzz::test_5を渡したら文字列Buzzを返す - Ass...
========================= 1 failed, 3 passed in 0.06s ==========================
サイクル4-2:テストを成功させる
3の倍数ができているので5の倍数も同様にできるはず
仮実装をせずにいきなり実装する
fizzbuzz.py
class FizzBuzz:
def convert(num):
if num % 3 == 0:
return 'Fizz'
elif num % 5 == 0:
return 'Buzz'
else:
return str(num)
テストを実行し、パスすることを確認
output
test_fizzbuzz.py::TestFizzBuzz::test_2を渡したら文字列2を返す PASSED [ 25%]
test_fizzbuzz.py::TestFizzBuzz::test_3を渡したら文字列Fizzを返す PASSED [ 50%]
test_fizzbuzz.py::TestFizzBuzz::test_5を渡したら文字列Buzzを返す PASSED [ 75%]
test_fizzbuzz.py::TestFizzBuzz::test_1を渡したら文字列1を返す PASSED [100%]
サイクル4-3:リファクタリングする
リファクタリングする箇所はなさそうなので終了
TODO
テスト容易性:高 重要度:高
- [x] 数を文字列に変換する
- [x] 1を渡すと文字列"1"を返す -> 仮実装
- [x] 2を渡すと文字列"2"を返す -> 三角測量
- [x] 3の倍数のときは数の代わりに「Fizz」に変換する
- [x] 3を渡すと文字列"Fizz"を返す -> 仮実装 -> 実装
- [x] 5の倍数のときは数の代わりに「Buzz」に変換する
- [x] 5を渡すと文字列"Buzz"を返す -> 明白な実装
- [] 3と5両方の倍数のときには数の代わりに「FizzBuzz」に変換する
テスト容易性:低 重要度:低
- [] 1からnまで
- [] 1から100まで
- [] プリントする
ライブコーディング:後編
テストの構造化
今のテストコードの状態では他の人が見たときに結局どういった仕様なのか実際のコードを見ないと読み解くことが難しい
TODOリストのようにテストコードを構造化しておくことで、他の人がテストコード及びその実行結果から仕様が理解できるようにしておく
また、仕様の集合として3の倍数の時、5の倍数の時、その他の時という3つの集合に分類できることに気づく
三角測量で用いた2を渡すケースは本来不要なケースなので消しておく
test_fizzbuzz.py
from fizzbuzz import FizzBuzz
class TestFizzBuzz:
class Test_3の倍数の場合:
def test_3を渡したら文字列Fizzを返す(self):
# 実行&検証
assert FizzBuzz.convert(3) == "Fizz"
class Test_5の倍数の場合:
def test_5を渡したら文字列Buzzを返す(self):
# 実行&検証
assert FizzBuzz.convert(5) == "Buzz"
class Test_その他の場合:
def test_1を渡したら文字列1を返す(self):
# 実行&検証
assert FizzBuzz.convert(1) == "1"
テストを実行し、パスすることを確認
output
test_fizzbuzz.py::TestFizzBuzz::Test_3の倍数の場合::test_3を渡したら文字列Fizzを返す PASSED [ 33%]
test_fizzbuzz.py::TestFizzBuzz::Test_5の倍数の場合::test_5を渡したら文字列Buzzを返す PASSED [ 66%]
test_fizzbuzz.py::TestFizzBuzz::Test_その他の場合::test_1を渡したら文字列1を返す PASSED [100%]
サイクル5-1:テストを実行して失敗させる
最後に残りの3と5の両方の倍数の時のロジックを追加する
TODO
テスト容易性:高 重要度:高
- [x] 数を文字列に変換する
- [x] 1を渡すと文字列"1"を返す -> 仮実装
- [x] 2を渡すと文字列"2"を返す -> 三角測量
- [x] 3の倍数のときは数の代わりに「Fizz」に変換する
- [x] 3を渡すと文字列"Fizz"を返す -> 仮実装 -> 実装
- [x] 5の倍数のときは数の代わりに「Buzz」に変換する
- [x] 5を渡すと文字列"Buzz"を返す -> 明白な実装
- [] 3と5両方の倍数のときには数の代わりに「FizzBuzz」に変換する
テスト容易性:低 重要度:低
- [] 1からnまで
- [] 1から100まで
- [] プリントする
テストコードにテストケースを追加する
test_fizzbuzz.py
from fizzbuzz import FizzBuzz
class TestFizzBuzz:
class Test_3の倍数の場合:
def test_3を渡したら文字列Fizzを返す(self):
# 実行&検証
assert FizzBuzz.convert(3) == "Fizz"
class Test_5の倍数の場合:
def test_5を渡したら文字列Buzzを返す(self):
# 実行&検証
assert FizzBuzz.convert(5) == "Buzz"
class Test_3と5の倍数の場合:
def test_15を渡したら文字列FIzzBuzzを返す(self):
# 実行&検証
assert FizzBuzz.convert(15) == "FizzBuzz"
class Test_その他の場合:
def test_1を渡したら文字列1を返す(self):
# 実行&検証
assert FizzBuzz.convert(1) == "1"
テストを実行し、追加したテストケースが失敗していることを確認
output
test_fizzbuzz.py::TestFizzBuzz::Test_3の倍数の場合::test_3を渡したら文字列Fizzを返す PASSED [ 25%]
test_fizzbuzz.py::TestFizzBuzz::Test_5の倍数の場合::test_5を渡したら文字列Buzzを返す PASSED [ 50%]
test_fizzbuzz.py::TestFizzBuzz::Test_3と5の倍数の場合::test_15を渡したら文字列FIzzBuzzを返す FAILED [ 75%]
test_fizzbuzz.py::TestFizzBuzz::Test_その他の場合::test_1を渡したら文字列1を返す PASSED [100%]
=================================== FAILURES ===================================
____________ TestFizzBuzz.Test_3と5の倍数の場合.test_15を渡したら文字列FIzzBuzzを返す ____________
self = <test_fizzbuzz.TestFizzBuzz.Test_3と5の倍数の場合 object at 0x7efd985f1160>
def test_15を渡したら文字列FIzzBuzzを返す(self):
# 実行&検証
> assert FizzBuzz.convert(15) == "FizzBuzz"
E AssertionError: assert 'Fizz' == 'FizzBuzz'
E - FizzBuzz
E + Fizz
test_fizzbuzz.py:15: AssertionError
=========================== short test summary info ============================
FAILED test_fizzbuzz.py::TestFizzBuzz::Test_3と5の倍数の場合::test_15を渡したら文字列FIzzBuzzを返す
========================= 1 failed, 3 passed in 0.07s ==========================
サイクル5-3:テストを成功させる
3と5の倍数の時というロジックを実装する
このロジックはif文の最初に持ってくる必要があるので注意
fizzbuzz.py
class FizzBuzz:
def convert(num):
if num % 3 == 0 and num % 5 == 0:
return 'FizzBuzz'
elif num % 3 == 0:
return 'Fizz'
elif num % 5 == 0:
return 'Buzz'
else:
return str(num)
テストを実行し、パスすることを確認
output
test_fizzbuzz.py::TestFizzBuzz::Test_3の倍数の場合::test_3を渡したら文字列Fizzを返す PASSED [ 25%]
test_fizzbuzz.py::TestFizzBuzz::Test_その他の場合::test_1を渡したら文字列1を返す PASSED [ 50%]
test_fizzbuzz.py::TestFizzBuzz::Test_5の倍数の場合::test_5を渡したら文字列Buzzを返す PASSED [ 75%]
test_fizzbuzz.py::TestFizzBuzz::Test_3と5の倍数の場合::test_15を渡したら文字列FIzzBuzzを返す PASSED [100%]
サイクル5-3:リファクタリングする
リファクタリングする箇所はなさそうなので終了
TODO
テスト容易性:高 重要度:高
- [x] 数を文字列に変換する
- [x] 1を渡すと文字列"1"を返す -> 仮実装
- [x] 2を渡すと文字列"2"を返す -> 三角測量
- [x] 3の倍数のときは数の代わりに「Fizz」に変換する
- [x] 3を渡すと文字列"Fizz"を返す -> 仮実装 -> 実装
- [x] 5の倍数のときは数の代わりに「Buzz」に変換する
- [x] 5を渡すと文字列"Buzz"を返す -> 明白な実装
- [x] 3と5両方の倍数のときには数の代わりに「FizzBuzz」に変換する
テスト容易性:低 重要度:低
- [] 1からnまで
- [] 1から100まで
- [] プリントする
おしまい
この記事が気に入ったらサポートをしてみませんか?