小さいテスト駆動開発¶
Red -> Green -> Refactor のサイクルを 小さい単位で何度も 繰り返すテスト駆動開発
Uncle Bob流 テスト駆動開発¶
『Clean Craftsmanship』第2章 (Kindle版 pp.52,53)
テストを書くまでは本番コードを書いてはならない。本番コードがないためにテストは失敗する。
失敗するテストやコンパイルできないテストを必要以上に書いてはならない。失敗を解決するには本番コードを書く。
失敗しているテストを解決する本番コードを必要以上に書いてはならない。テストがパスしたら、追加のテストを書く。
この3原則により、小さい単位で進む
4つ目はリファクタリング
ハンズオンの題材 FizzBuzz¶
皆さんが日々取り組まれているプログラムからすると おもちゃ のような感じかもしれません
FizzBuzzであることは薄目で見ましょう! 「小さい」という視点で 抽出 した要素をふだんのプログラミングに活かせると考えています
環境構築¶
$ # examplesにいる前提です
$ cd tdd
.
├── fizzbuzz/
│ └── __init__.py
└── tests/
├── __init__.py
└── test_fizzbuzz.py
環境構築できているか確認
class TestFizzBuzz:
def test_fail(self):
assert False
pytest と叩きます。 テストが失敗したら環境構築できてます!
Tip
pytestのアサーション
pytestでは、Pythonの assert文に式を渡す
式が
True
と評価されれば、テストはパス式が
False
と評価されれば、テスト失敗(fail)
注釈
Uncle Bobは何もしないテストで確認する
class TestFizzBuzz:
def test_nothing(self):
...
Tip
Pythonで何もしない
pass文 https://docs.python.org/ja/3/reference/simple_stmts.html#the-pass-statement
pass は、構文法的には文が必要だが、コードとしては何も実行したくない場合のプレースホルダとして有用です。
Ellipsis
...
https://docs.python.org/ja/3/reference/datamodel.html#ellipsispass
の代わりに使えます
TODOリスト¶
FizzBuzzの仕様を整理
- [ ] 数をそのまま文字列に変換する
- [ ] 3の倍数のときは数の代わりに「Fizz」に変換する
- [ ] 5の倍数のときは数の代わりに「Buzz」に変換する
- [ ] 15の倍数のときは数の代わりに「FizzBuzz」に変換する
ref: TDD Boot Camp 2020 Online #1 基調講演/ライブコーディング (55:54)
TDDBCのライブコーディングからは以下が参考になります
TODOリストの作り方
重要度の付け方(重要度が高く、テストしやすい 項目を実現する)
1サイクル¶
- [ ] 数をそのまま文字列に変換する
- [ ] 1を渡すと文字列1を返す
- [ ] 3の倍数のときは数の代わりに「Fizz」に変換する
- [ ] 5の倍数のときは数の代わりに「Buzz」に変換する
- [ ] 15の倍数のときは数の代わりに「FizzBuzz」に変換する
1を渡すと文字列1を返す
Q: どのようにやりますか?
警告
かつてのnikkieの場合
テストを全部書き切る
テストが落ちているので実装を書く
リファクタリングして次のケースへ
ptw とコマンドを叩き、ファイルの変更のたびにテストが流れるようにします。 Ctrl+C で抜けられます
Red🟥 ImportError¶
from fizzbuzz import FizzBuzz
class TestFizzBuzz:
def test_1を渡すと文字列1を返す(self):
...
from fizzbuzz import FizzBuzz
E ImportError: cannot import name 'FizzBuzz' from 'fizzbuzz' (/.../small-technical-2023/examples/tdd/fizzbuzz/__init__.py)
テストで使いたいクラスがまだないため、それをimportできないというエラー
3原則の1「本番コードがないためテスト失敗」となったので、2「失敗を解決するために本番コードを書く」へ進む
Green🟩 ImportError解決¶
class FizzBuzz:
...
Tip
__init__.py
__init__.py
が置かれたディレクトリは パッケージ となるパッケージ
pkgA
の__init__.py
のFoo
クラスはfrom pkgA import Foo
とimportできるpkgA
のfoo.py
のFoo
クラスはfrom pkgA.foo import Foo
原則3「テストがパスしたら、追加のテストを書く」より、本番コードには何もしないクラスを書くだけ
Red🟥 AttributeError¶
importできるようになったので、アサーションを書く
from fizzbuzz import FizzBuzz
class TestFizzBuzz:
def test_1を渡すと文字列1を返す(self):
fizz_buzz = FizzBuzz()
assert "1" == fizz_buzz.convert(1)
def test_1を渡すと文字列1を返す(self):
fizz_buzz = FizzBuzz()
> assert "1" == fizz_buzz.convert(1)
E AttributeError: 'FizzBuzz' object has no attribute 'convert'
インスタンス化はできている
convert
メソッドの呼び出しでAttributeError
送出
3原則の1「本番コードがないためテスト失敗」となったので、2「失敗を解決するために本番コードを書く」へ
AttributeError解決¶
実装して、convertメソッドを定義
class FizzBuzz:
def convert(self, n: int) -> str:
return ""
🟥 AssertionError
def test_1を渡すと文字列1を返す(self):
fizz_buzz = FizzBuzz()
> assert "1" == fizz_buzz.convert(1)
E AssertionError: assert '1' == ''
E + 1
まだ 1「本番コードがないためテスト失敗」の状態
Green🟩 仮実装¶
class FizzBuzz:
def convert(self, n: int) -> str:
return "1"
通った。リファクタリングは特になし
テストがパスしたので次のテストへ
仮実装で テストのテスト をした。 落ちるべきときに落ち、通るべきときに通る(TDDBCより)
実装が
"1"
を返すときにテストが通る実装が
"1"
を返さない(最初は空文字列)ならばテストは通らない
2サイクル¶
キーワード:三角測量
仮実装した1例がある
実装を 一般化するために2例目を追加
2例のテストを通すよう実装を一般化
- [ ] 数をそのまま文字列に変換する
- [x] 1を渡すと文字列1を返す
- [ ] 2を渡すと文字列2を返す
- [ ] 3の倍数のときは数の代わりに「Fizz」に変換する
- [ ] 5の倍数のときは数の代わりに「Buzz」に変換する
- [ ] 15の倍数のときは数の代わりに「FizzBuzz」に変換する
Red🟥 2のケース¶
class TestFizzBuzz:
def test_2を渡すと文字列2を返す(self):
fizz_buzz = FizzBuzz()
assert "2" == fizz_buzz.convert(2)
2件のうち1件通り、1件落ちる 🟩🟥
Green🟩 2のケース¶
2件通すように実装を修正
class FizzBuzz:
def convert(self, n: int) -> str:
return str(n)
通った🙌
Refactor 分かりやすい変数名に¶
引数 n
を number
に変えて分かりやすくする
VSCodeの Rename Symbol を利用
class FizzBuzz:
def convert(self, number: int) -> str:
return str(number)
3サイクル¶
- [ ] 数をそのまま文字列に変換する
- [x] 1を渡すと文字列1を返す
- [x] 2を渡すと文字列2を返す
- [ ] 3の倍数のときは数の代わりに「Fizz」に変換する
- [ ] 3を渡すと文字列Fizzを返す
- [ ] 5の倍数のときは数の代わりに「Buzz」に変換する
- [ ] 15の倍数のときは数の代わりに「FizzBuzz」に変換する
Red🟥 3のケース¶
class TestFizzBuzz:
def test_3を渡すと文字列Fizzを返す(self):
fizz_buzz = FizzBuzz()
assert "Fizz" == fizz_buzz.convert(3)
2件通り1件落ちる 🟩🟩🟥
Green🟩 3のケース(仮実装)¶
class FizzBuzz:
def convert(self, number: int) -> str:
if number == 3:
return "Fizz"
return str(number)
テスト3件通る🙌
Refactor テストコードの重複除去¶
テストコードにはインスタンス化の重複がある。 pytestの フィクスチャ を使って インスタンス化を1箇所に まとめる
VS Codeの Extract method が使える。 ここではクラスに抽出(モジュールレベルで抽出しても動きます)
import pytest
class TestFizzBuzz:
@pytest.fixture
def fizz_buzz(self) -> FizzBuzz:
return FizzBuzz()
定義したフィクスチャを各テスト(メソッド)で利用
class TestFizzBuzz:
def test_3を渡すと文字列Fizzを返す(self, fizz_buzz):
assert "Fizz" == fizz_buzz.convert(3)
倍数のときへと一般化¶
これまでのサイクルのおかげで自信が少しはあるので、三角測量を経ずに一般化
class FizzBuzz:
def convert(self, number: int) -> str:
if number % 3 == 0:
return "Fizz"
return str(number)
4サイクル¶
- [ ] 数をそのまま文字列に変換する
- [x] 1を渡すと文字列1を返す
- [x] 2を渡すと文字列2を返す
- [ ] 3の倍数のときは数の代わりに「Fizz」に変換する
- [x] 3を渡すと文字列Fizzを返す
- [ ] 5の倍数のときは数の代わりに「Buzz」に変換する
- [ ] 5を渡すと文字列Buzzを返す
- [ ] 15の倍数のときは数の代わりに「FizzBuzz」に変換する
Red🟥 5のケース¶
class TestFizzBuzz:
def test_5を渡すと文字列Buzzを返す(self, fizz_buzz):
assert "Buzz" == fizz_buzz.convert(5)
3件通り1件落ちる 🟩🟩🟩🟥
Green🟩 5のケース(明白な実装)¶
明白な実装(ここまで開発してきてテストコードには 自信 がある)
class FizzBuzz:
def convert(self, number: int) -> str:
if number % 3 == 0:
return "Fizz"
if number % 5 == 0:
return "Buzz"
return str(number)
4件通る🙌
5サイクル¶
- [ ] 数をそのまま文字列に変換する
- [x] 1を渡すと文字列1を返す
- [x] 2を渡すと文字列2を返す
- [ ] 3の倍数のときは数の代わりに「Fizz」に変換する
- [x] 3を渡すと文字列Fizzを返す
- [ ] 5の倍数のときは数の代わりに「Buzz」に変換する
- [x] 5を渡すと文字列Buzzを返す
- [ ] 15の倍数のときは数の代わりに「FizzBuzz」に変換する
- [ ] 15を渡すと文字列FizzBuzzを返す
Red🟥 15のケース¶
class TestFizzBuzz:
def test_15を渡すと文字列FizzBuzzを返す(self, fizz_buzz):
assert "FizzBuzz" == fizz_buzz.convert(15)
3件通り1件落ちる 🟩🟩🟩🟩🟥
Green🟩 15のケース¶
class FizzBuzz:
def convert(self, number: int) -> str:
if number % 3 == 0 and number % 5 == 0:
return "FizzBuzz"
if number % 3 == 0:
return "Fizz"
if number % 5 == 0:
return "Buzz"
return str(number)
5件通る🙌🙌
- [ ] 数をそのまま文字列に変換する
- [x] 1を渡すと文字列1を返す
- [x] 2を渡すと文字列2を返す
- [ ] 3の倍数のときは数の代わりに「Fizz」に変換する
- [x] 3を渡すと文字列Fizzを返す
- [ ] 5の倍数のときは数の代わりに「Buzz」に変換する
- [x] 5を渡すと文字列Buzzを返す
- [ ] 15の倍数のときは数の代わりに「FizzBuzz」に変換する
- [x] 15を渡すと文字列FizzBuzzを返す
考察¶
ケント・ベックの定義(2つのルール 『テスト駆動開発』Kindle の位置No.33-34)
自動化されたテストが失敗したときのみ、新しいコードを書く。
重複を除去する。
Red -> Green -> Refactor
Uncle Bobの3原則でTDDを進めると、 strict になった
Before(小さいテスト駆動開発を知る前)¶
1のケースのテストコードを一気に全部書く(Red🟥)
1のケースのテストを通す実装を全部書く(Green🟩)
リファクタリング
2のケースのテストコードへ
1つのテストケースに対してRedとGreenは1つずつ。
小さいテスト駆動開発¶
これに対して、ここで見た小さいテスト駆動開発では、RedとGreenを細かく行き来する
1のケースのテストコードをエラーが出るまで書く(Red🟥)
上記のエラーを解決する実装を書く(Green🟩)
リファクタリング
1のケースのテストコードの続きをエラーが出るまで書く(Red🟥)
上記のエラーを解決する実装を書く(Green🟩)
リファクタリング
1のケースのテストコードの続きをエラーが出るまで書く(Red🟥)
上記のエラーを解決する実装を書く(Green🟩)
リファクタリング
2のケースのテストコードへ
テストも実装も 少しずつ できあがっていく!
この先¶
動作するドキュメントにする¶
TDD Boot Campでは、t-wadaさんはこのあと、テストコードを 動作するドキュメント にしていく
TODOリスト(仕様)のネスト構造をテストコードに反映
テストコードで クラスがネスト する
テストを 最小限 にしておく
三角測量で作ったテストは片方 消す (書いた人だけが三角測量で進んだから似たテストがあると分かっている)
モックを使ったTDDの例¶
『ちょうぜつ本(ちょうぜつソフトウェア設計入門)』より、FizzBuzzを シンプルな設計 で実装する
畳み込み演算 と看破した例
参考文献¶
『テスト駆動開発』
仮実装・明白な実装・三角測量(第1章〜第3章)