分かること
・テスト駆動開発(TDD)はどの順序で開発すべきか?
・テスト駆動開発(TDD)はなぜ失敗をわざと行うのか?
・pytestはどう使うべきか?
目次
1. テスト駆動開発(TDD)について
2. Python, Pytestでテスト駆動開発(FizzBuzz問題)
1. テスト駆動開発(TDD)について
テスト駆動開発は以下3種のサイクルである。
(ToDoリストを作成し、簡単な処理から手を付けると良い)
1. RED(テスト失敗)
テストコードを作成してテストする。(実装コードを作成してないので失敗する)
2. GREEN(テスト成功)
成功するように必要最小限の実装コードを書く。(正しさや綺麗さは考えない)
3. リファクタリング
成功を維持したままコードを綺麗にする
(画像:50 分でわかるテスト駆動開発 より)
なぜ失敗を工程に入れるのか?
テストコードの正しさを確認(テストコードのテスト)する為。
誤りのある実装コードのテストが予想通りのエラーならば、正しい実装コードでもテストが正しく機能されると期待できる。
2. Pytestでテスト駆動開発(FizzBuzz問題)
FizzBuzz問題をテスト駆動開発で実装し、テスト駆動開発の工程を確認する。
FizzBuzz問題
1から100までの数をプリントするプログラムを書け。
ただし
3の倍数のときは数の代わりに「Fizz」とプリントし、
5の倍数のときは「Buzz」とプリントし、
3と5両方の倍数の場合には「FizzBuzz」とプリントすること。
0. [準備] ToDoリストを作成する
簡単な処理から優先順位を高くすると良い。
- [ ] 数を文字列にして返す
- [ ] 3の倍数のときは数の代わりに文字列'Fizz文字列'と返す
- [ ] 5の倍数のときは文字列'Buzz'と返す
- [ ] 3と5の倍数の場合には文字列'FizzBuzz'と返す
- [ ] 1から100までの数
- [ ] プリントする
1. ToDoリストから1つをピックアップしてサイクルを回す
- [ ] 数を文字列にして返す
- [ ] 1を渡したら文字列"1"を返す ← next
- [ ] 2を渡したら文字列"2"を返す
必要ならToDoの深堀りをする。
1-1. RED(テスト失敗)
テストコードを作成してテストでわざと失敗する
1-1-1. テストコードを作成(test_fizz_buzz.py)
「1を渡したら文字列"1"を返す」テストを作成する
import pytest
class TestStringConvert:
# 前準備
@pytest.fixture
def target(self):
from fizz_buzz import string_convert
return string_convert
# 検証
def test_return_value(self, target):
assert target(1) == '1'
この時点で既に、
・実装ファイルとテストコードのファイル名(fizz_buzz.py, test_fizz_buzz.py)
・数を文字列にして返す関数名
・数を文字列にして返す処理をテストするクラスと関数(TestStringConvert, test_return_value)
と考えることが多い為、ToDoは簡単な処理から始めるのがおすすめ。
"@pytest.fixture"はテストの前処理(実装コードで使う関数の呼び出し)を行うpytestの機能。
1-1-2. テストコードをテスト(test_fizz_buzz.py)
実装コードを作成してない(実装モジュールを作成してない)ので失敗する
pytest test_fizz_buzz.py
============================= test session starts =============================
-
collected 1 item
test_fizz_buzz.py E [100%]
=================================== ERRORS ====================================
____________ ERROR at setup of TestStringConvert.test_return_value ____________
self = <test_fizz_buzz.TestStringConvert object at 0x000001EF5648A190>
@pytest.fixture
def target(self):
> from fizz_buzz import string_convert
E ModuleNotFoundError: No module named 'fizz_buzz'
test_fizz_buzz.py:7: ModuleNotFoundError
=========================== short test summary info ===========================
ERROR test_fizz_buzz.py::TestStringConvert::test_return_value - ModuleNotFoun...
========================= 1 error in 0.18s =========================
予想(狙い)通り、fizz_buzzモジュールがないエラーなら成功。
E ModuleNotFoundError: No module named 'fizz_buzz'
このエラーが出なければ、テストコードのどこかに誤りがあると分かる。
1-2. GREEN(テスト成功)
テストコードが成功することだけを優先して必要最小限の実装コードを書く。
(この時点で正しさや綺麗さは考えなくて良い)
1-2-1. 必要最小限の実装コードを書く(fizz_buzz.py)
def string_convert(x):
return '1'
1-2-2. テストコードをテスト(test_fizz_buzz.py)
pytest test_fizz_buzz.py
============================= test session starts =============================
-
collected 1 item
test_fizz_buzz.py . [100%]
======================== 1 passed in 0.02s =========================
成功。
(1-3. リファクタリング)
今回はそのまま次のサイクルへ向かう。
2. 次のサイクルを回す
- [ ] 数を文字列にして返す
- [x] 1を渡したら文字列"1"を返す
- [ ] 2を渡したら文字列"2"を返す ← next
2-1. RED(テスト失敗)
2-1-1. テストコードを拡張(test_fizz_buzz.py)
「2を渡したら文字列"2"を返す」テストを追加する
import pytest
class TestStringConvert:
# 前準備
@pytest.fixture
def target(self):
from fizz_buzz import string_convert
return string_convert
+ check_value_data = [
+ (1, '1'), # 1を渡したら文字列"1"を返す
+ (2, '2') # 2を渡したら文字列"2"を返す
+ ]
# 検証
- def test_return_value(self, target):
- assert target(x) == '1'
+ @pytest.mark.parametrize('x, expected_return_value', check_value_data)
+ def test_return_value(self, target, x, expected_return_value):
+ assert target(x) == expected_return_value
@pytest.mark.parametrizeで以下2パターン(計2回)のテストを定義する。
1.x = 1, expected_return_value= '1'
2.x = 2, expected_return_value= '2'
→1つのテストに複数のassert文を記述するのは避ける。
以下のような記述は避ける。
(assert1でエラーとなったとき、assert2のテスト結果が分からない為。)
# 検証(避けたい記述方法)
def test_return_value(self, target):
assert target(1) == '1' # assert1
assert target(2) == '2' # assert2(assert1がエラーだと実行されない)
2-1-2. テストコードをテスト(test_fizz_buzz.py)
現在の実装コード(fizz_buzz.py)はどんな値を入れても文字列1を返す為、エラーが出る。
pytest test_fizz_buzz.py
============================= test session starts =============================
-
collected 2 items
test_fizz_buzz.py .F [100%]
================================== FAILURES ===================================
__________________ TestStringConvert.test_return_value[2-2] ___________________
self = <test_fizz_buzz.TestStringConvert object at 0x0000013B7BC5C4F0>
target = <function string_convert at 0x0000013B7BC32DC0>, x = 2
expected_return_value = '2'
@pytest.mark.parametrize('num, expected_return_value', check_value_data)
def test_return_value(self, target, x, expected_return_value):
> assert target(x) == expected_return_value
E AssertionError: assert '1' == '2'
E - 2
E + 1
test_fizz_buzz.py:18: AssertionError
=========================== short test summary info ===========================
FAILED test_fizz_buzz.py::TestStringConvert::test_return_value[2-2] - Asserti...
=================== 1 failed, 1 passed in 0.19s ====================
2-2. GREEN(テスト成功)
テストの(狙い通りの)失敗を確認後、テストが成功するように実装コードを訂正する。
def string_convert(x):
- return '1'
+ return str(x)
pytest test_fizz_buzz.py
============================= test session starts =============================
platform win32 -- Python 3.8.5, pytest-6.1.1, py-1.9.0, pluggy-0.13.1
rootdir: C:\Users\spell\Documents\GIT\fizzbuzz
collected 2 items
test_fizz_buzz.py .. [100%]
======================== 2 passed in 0.02s =========================
2つのテストが実行されたことも分かる。
collected 2 items
2-3. リファクタリング)
成功を維持したままコードを綺麗にする
import pytest
class TestStringConvert:
@pytest.fixture
def target(self):
from fizz_buzz import string_convert
return string_convert
check_value_data = [
(1, '1'),
(2, '2')
]
- @pytest.mark.parametrize('x, expected_return_value', check_value_data)
- def test_return_value(self, target, x, expected_return_value):
- assert target(x) == expected_return_value
+ @pytest.mark.parametrize('num, expected_return_value', check_value_data)
+ def test_return_value(self, target, num, expected_return_value):
+ assert target(num) == expected_return_value
- def string_convert(x):
- return str(x)
+ def string_convert(num):
+ return str(num)
3. 次のサイクルを回す
- [X] 数を文字列にして返す ← finish
- [X] 1を渡したら文字列'1'を返す
- [X] 2を渡したら文字列'2'を返す
- [ ] 3の倍数のときは数の代わりに文字列'Fizz'と返す
- [ ] 3を渡したら文字列'Fizz'を返す ← next
- [ ] 5の倍数のときは文字列'Buzz'と返す
- [ ] 3と5の倍数の場合には文字列'FizzBuzz'と返す
- [ ] 1から100までの数
- [ ] プリントする
3-1. RED(テスト失敗)
3-1-1. テストコードを拡張(test_fizz_buzz.py)
「3を渡したら文字列"Fizz"を返す」テストを追加する
import pytest
class TestStringConvert:
@pytest.fixture
def target(self):
from fizz_buzz import string_convert
return string_convert
check_value_data = [
(1, '1'),
(2, '2'),
(3, 'Fizz')
]
@pytest.mark.parametrize('num, expected_return_value', check_value_data)
def test_return_value(self, target, num, expected_return_value):
assert target(num) == expected_return_value
3-2. GREEN(テスト成功)
3-2-1. 成功するように必要最小限のコードを書く(倍数関係なく3ならFizzを返す)
def string_convert(num):
+ if num == 3:
+ return 'Fizz'
return str(num)
3-2-2. 成功したら正しい処理(3の倍数ならFizzを返す)をするよう訂正
def string_convert(num):
- if num == 3:
+ if num % 3 == 0:
return 'Fizz'
return str(num)
3-3. リファクタリング
・テストコードから仕様が読み取れない(個別の具体的な動きしかない)為、テストコードだけで仕様を読み取れるようにする。
・また、テストのパターン数に対称性を持たせる。
(メンテナンスを減らす為にも場合分けは減らして対称性を持たせる)
import pytest
class TestStringConvert:
@pytest.fixture
def target(self):
from fizz_buzz import string_convert
return string_convert
check_value_data = [
- (1, '1'),
- (2, '2'),
+ # 3の倍数のとき
(3, 'Fizz'),
+ # その他の数のとき
+ (1, '1')
]
@pytest.mark.parametrize('num, expected_return_value', check_value_data)
def test_return_value(self, target, num, expected_return_value):
assert target(num) == expected_return_value
今回はコメントアウトで説明したが、以下の手法でも効果的。
・テスト関数名を長くして仕様を説明する。
・パターンごとにテストクラス分けを行う。
以上を繰り返すのがテスト駆動開発(TDD)である。
参考動画:50 分でわかるテスト駆動開発(@t_wadaさんによるライブコーディング)