1. 概要
1.1. 実施日時
1.2. 会場
1.4. お題
1.5. 言語
-
Python 3.6
2. セッション
2.1. TODOリストから始めるテスト駆動開発
2.1.1. TODOリスト
テストおじさん: はい、第1回 Re:ゼロから始めるテスト駆動開発生活 はじめます、レポジトリは ここね。
テストおじさん: まず、皆さんVSCodeでセッションの共有をお願いしますね。
ちなみにVisual Studio Live Shareの利用イメージは こんな感じ。
今回のセッションはテストおじさんの端末を参加者で共有してテスト駆動でどのように お題 を実装していくかをライブデモします。
テストおじさん: まずはお題の仕様の確認と TODOリスト の作成をします。
そう言うとおもむろに REAME.mdに以下の文章を追記するテストおじさん。
-
❏ 値が3ならばFizzを返すようにする
-
❏ 値が5ならばBuzzを返すようにする
-
❏ 値が15ならばFizzBuzzを返すようにする
TODOリスト
何をテストすべきだろうか----着手する前に、必要になりそうなテストをリストに書き出しておこう。
2.2. テストファーストから始めるテスト駆動開発
2.2.1. テストファースト・アサートファースト
テストおじさん: 次に テストファースト でやります。
ややぎごちない動作で main_test.py
に以下のコードを記述するテストおじさん。
import unittest
class FizzBuzzTest(unittest.TestCase):
def test_値が3ならばFizzを返すようにする(self):
self.assertEqual("Fizz", 3)
if __name__ == "__main__":
unittest.main()
テストおじさん: テストを実行してみましょう・・・はい、失敗。
$ python main_test.py
F
======================================================================
FAIL: test_値が3ならばFizzを返すようにする (__main__.FizzBuzzTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "main_test.py", line 7, in test_値が3ならばFizzを返すようにする
self.assertEqual("Fizz", 3)
AssertionError: 'Fizz' != 3
----------------------------------------------------------------------
Ran 1 test in 0.000s
FAILED (failures=1)
テストおじさん以外のメンバー: ポカーン( ゚д゚)
テスト書きたい人: …実装する前に、テストコードを確認するのですか?
テストおじさん: イエス、今やっていることはテストといいながら設計をしています。
テストおじさん: 実際、最初のテスト項目は TODOリスト の内容をそのまま転記してますからね。
テストおじさん: テスト駆動開発の進め方として テストファースト ・ アサートファースト で進めていきます。
テストファースト
いつテストを書くべきだろうか----それはテスト対象のコードを書く前だ。
アサートファースト
いつアサーションを書くべきだろうか----最初に書こう
システム構築はどこから始めるべきだろうか。システム構築が終わったらこうなる、というストーリーを語るところからだ。
機能はどこから書き始めるべきだろうか。コードが書き終わったらこのように動く、というテストを書くところからだ。
ではテストはどこから書き始めるべきだろうか。それはテストの終わりにパスすべきアサーションを書くところからだ。
テスト書いてないとt_wadaさんの前で言った人: class FizzBuzzTest(unittest.TestCase):
の表記は?
テストおじさん: これは unittest.TestCase
クラスを継承した FizzBuzzTest
クラスという意味です。
テスト書いてないとt_wadaさんの前で言った人: Pythonは()の中に継承を記述するのね。
テストおじさん: そっすね、ちなみにPythonは多重継承もサポートしているので複数のクラスを列挙できます。
テストおじさん: なんというかPythonのクラスの継承はカルチャーギャップがありました。
注:テスト書いてないとt_wadaさんの前で言った人もテストおじさんもRubyがメインの人。
テストおじさん: ちなみにRubyだとこんな感じ。
require 'minitest/autorun'
class FizzBuzzSpec < Minitest::Spec
describe FizzBuzz do
it '3ならFizzを返す' do
expect(3).must_equal 'Fizz'
end
end
end
テストおじさん: さて次は失敗するテストをパスするようにしないといけませんね。
2.3. 仮実装から始めるテスト駆動開発
2.3.1. 仮実装
テストおじさん: 最初のテストを通すために 仮実装 を実施します。
import unittest
class FizzBuzz:
@staticmethod
def generate(number):
return "Fizz"
class FizzBuzzTest(unittest.TestCase):
def test_値が3ならばFizzを返すようにする(self):
self.assertEqual("Fizz", FizzBuzz.generate(3))
if __name__ == "__main__":
unittest.main()
テストおじさん: テストを実行してみましょう・・・はい、成功。
$ python main_test.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
仮実装を経て本実装へ
失敗するテストを書いてから、最初に行う実装はどのようなものだろうか----ベタ書きの値を返そう。
テストおじさん以外のメンバー: これが実装だと…!?
テストおじさん: これで実装完了・・・とはいきませんね、ロジックを実装して本当にテストがパスするようにしましょう。
import unittest
class FizzBuzz:
@staticmethod
def generate(number):
value = number
if number % 3 == 0:
value = "Fizz"
return value
class FizzBuzzTest(unittest.TestCase):
def test_値が3ならばFizzを返すようにする(self):
self.assertEqual("Fizz", FizzBuzz.generate(3))
if __name__ == "__main__":
unittest.main()
テストおじさん: テストを実行してみましょう・・・はい、成功。
$ python main_test.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
2.3.2. 三角測量
テストおじさん: 1つ目のテストがパスしたので TODOリスト からテストを追加してみましょうか。
import unittest
class FizzBuzz:
@staticmethod
def generate(number):
value = number
if number % 3 == 0:
value = "Fizz"
return value
class FizzBuzzTest(unittest.TestCase):
def test_値が3ならばFizzを返すようにする(self):
self.assertEqual("Fizz", FizzBuzz.generate(3))
if __name__ == "__main__":
unittest.main()
テストおじさん: テストを実行してみましょう・・・はい、失敗。
$ python main_test.py
.F
======================================================================
FAIL: test_値が5ならばBuzzを返すようにする (__main__.FizzBuzzTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "main_test.py", line 21, in test_値が5ならばBuzzを返すようにする
self.assertEqual("Buzz", FizzBuzz.generate(5))
AssertionError: 'Buzz' != 5
----------------------------------------------------------------------
Ran 2 tests in 0.000s
FAILED (failures=1)
テストおじさん: あー対応する処理がないからテストが失敗してますな。
テストおじさん: 修正修正・・・
import unittest
class FizzBuzz:
@staticmethod
def generate(number):
value = number
if number % 3 == 0:
value = "Fizz"
elif number % 5 == 0:
value = "Buzz"
return value
class FizzBuzzTest(unittest.TestCase):
def test_値が3ならばFizzを返すようにする(self):
self.assertEqual("Fizz", FizzBuzz.generate(3))
def test_値が5ならばBuzzを返すようにする(self):
self.assertEqual("Buzz", FizzBuzz.generate(5))
if __name__ == "__main__":
unittest.main()
テストおじさん: テストを実行してみましょう・・・はい、OK。
$ python main_test.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
テストおじさん: 2つ目のテストによってgenerateメソッドの一般化が実現できました。
テストおじさん: このようなアプローチを 三角測量 といいます。
三角測量
テストから最も慎重に一般化を引き出すやり方はどのようなものだろうか----2つ以上の例があるときだけ、一般化を行うようにしよう。
テストおじさん: はい、残りの TODOリスト の内容もサクッと片付けちゃいましょう。
import unittest
class FizzBuzz:
@staticmethod
def generate(number):
value = number
if number % 3 == 0:
value = "Fizz"
elif number % 5 == 0:
value = "Buzz"
elif number % 3 == 0 and number % 5 == 0:
value = "FizzBuzz"
return value
class FizzBuzzTest(unittest.TestCase):
def test_値が3ならばFizzを返すようにする(self):
self.assertEqual("Fizz", FizzBuzz.generate(3))
def test_値が5ならばBuzzを返すようにする(self):
self.assertEqual("Buzz", FizzBuzz.generate(5))
def test_値が15ならばFizzBuzzを返すようにする(self):
self.assertEqual("FizzBuzz", FizzBuzz.generate(15))
if __name__ == "__main__":
unittest.main()
テストおじさん: テストを実行してみましょう・・・はい、オッケい!?。
$ python main_test.py
F..
======================================================================
FAIL: test_値が15ならばFizzBuzzを返すようにする (__main__.FizzBuzzTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "main_test.py", line 28, in test_値が15ならばFizzBuzzを返すようにする
self.assertEqual("FizzBuzz", FizzBuzz.generate(15))
AssertionError: 'FizzBuzz' != 'Fizz'
- FizzBuzz
+ Fizz
----------------------------------------------------------------------
Ran 3 tests in 0.000s
FAILED (failures=1)
テストおじさん: おっと、意図した結果と違いますね。デバッガで確認してみましょう。
VSCodeのデバッガ を起動してステップ実行で条件分岐を確認するテストおじさん。
class FizzBuzz:
@staticmethod
def generate(number):
value = number
if number % 3 == 0: (1)
value = "Fizz"
elif number % 5 == 0:
value = "Buzz"
elif number % 3 == 0 and number % 5 == 0: (2)
value = "FizzBuzz"
return value
テストおじさん: number
が15の場合は最初の判定で処理を抜けてますね、ここはこうかな。
class FizzBuzz:
@staticmethod
def generate(number):
value = number
if number % 3 == 0 and number % 5 == 0: (2)
value = "FizzBuzz"
elif number % 3 == 0: (1)
value = "Fizz"
elif number % 5 == 0:
value = "Buzz"
return value
テストおじさん: テストを再実行してみましょう・・・はい、OK。
$ python main_test.py
...
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK
テストおじさん: はい、これでTODOリストの内容を完了することができました。
-
✓ 値が3ならばFizzを返すようにする
-
✓ 値が5ならばBuzzを返すようにする
-
✓ 値が15ならばFizzBuzzを返すようにする
仕様を眺めながら
テストおじさん: あー仕様にそれ以外では値を返すというのがあるのでテストに明記しておきましょう。
def test_値が100ならば100を返すようにする(self):
self.assertEqual(100, FizzBuzz.generate(100))
テスト書いてないとt_wadaさんの前で言った人: その条件だと・・・
ニヤリと笑うテストおじさん。
テストおじさん: 実行・・・はい、仕様を書いた人が間違ってましたね。
$ python main_test.py
F...
======================================================================
FAIL: test_値が100ならば100を返すようにする (__main__.FizzBuzzTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "main_test.py", line 31, in test_値が100ならば100を返すようにする
self.assertEqual(100, FizzBuzz.generate(100))
AssertionError: 100 != 'Buzz'
----------------------------------------------------------------------
Ran 4 tests in 0.000s
FAILED (failures=1)
テストおじさん: ここはこうで・・・
def test_値が101ならば101を返すようにする(self):
self.assertEqual(101, FizzBuzz.generate(101))
テストおじさん: こう!
$ python main_test.py
....
----------------------------------------------------------------------
Ran 4 tests in 0.000s
OK
2.3.3. オブジェクト指向
テスト書きたい人: 実務でもテスト書いてますか?
テストおじさん: イエス!自社の基幹業務システム(Ruby on Rails)は自分で作ったけど一応自動テストは完備してますよ。
解説しよう!テストおじさんはユーザー企業のぼっち社内プログラマなのだ。
テスト書きたい人: テスト駆動を実践してる現場って見たことないんですよね〜
テストしてない人: ですよね〜
テスト書いてないとt_wadaさんの前で言った人: GitHubに公開されているプロダクトなんかはテストコードついてますけどね。
テストおじさん: 正直テストコードを書くのは最初はしんどいけど後になるほど楽になります。
テスト書きたい人: ことろで @staticmethod
というのは?
テストおじさん: スタティックメソッドのアノテーションなんですがスタティックメソッドはインスタンスメソッドとは違って・・・
この後クラスの概念とかいろいろディスカッションする。
テストおじさん: ・・・オブジェクトシコウムズカシイネ。
2.3.4. TODOリスト再び
テストおじさん: さて、残りの仕様を実現するために TODOリスト を更新しましょうか
-
✓ 値が3ならばFizzを返すようにする
-
✓ 値が5ならばBuzzを返すようにする
-
✓ 値が15ならばFizzBuzzを返すようにする
-
❏ 複数回実行されたら結果を返すようにする
テストおじさん: これは少し複雑な感じがしますね、まずはテストコードを通して振る舞いを考えてましょう。
def test_回数を5回実行すると配列を返すようにする(self):
self.assertEqual([1, 2, "Fizz", 4, "Buzz"], FizzBuzz.iterate(5))
テストおじさん: 引数に回数を取り戻り値に配列を返す振る舞いを設計しました。
テストおじさん: おわかりいただけだろうか・・・
テストおじさん: テストと言いながら設計をしているのです、そうテストではなく設計を!
皮肉なことに、TDDはテスト技法ではない(Cunninghamの公案)。TDDは分析技法であり、設計技法であり、実際には開発のすべてのアクティビティを構造化する技法なのだ。
テストおじさん以外のメンバー: お、おう・・・
テストおじさん: ・・・えーと、実装に移りますね。ここはセオリー通り仮実装。
import unittest
class FizzBuzz:
@staticmethod
def generate(number):
value = number
if number % 3 == 0 and number % 5 == 0:
value = "FizzBuzz"
elif number % 3 == 0:
value = "Fizz"
elif number % 5 == 0:
value = "Buzz"
return value
@staticmethod
def iterate(count):
return [1, 2, "Fizz", 4, "Buzz"]
class FizzBuzzTest(unittest.TestCase):
def test_値が3ならばFizzを返すようにする(self):
self.assertEqual("Fizz", FizzBuzz.generate(3))
def test_値が5ならばBuzzを返すようにする(self):
self.assertEqual("Buzz", FizzBuzz.generate(5))
def test_値が15ならばFizzBuzzを返すようにする(self):
self.assertEqual("FizzBuzz", FizzBuzz.generate(15))
def test_値が101ならば101を返すようにする(self):
self.assertEqual(101, FizzBuzz.generate(101))
def test_回数を5回実行すると配列を返すようにする(self):
self.assertEqual([1, 2, "Fizz", 4, "Buzz"], FizzBuzz.iterate(5))
if __name__ == "__main__":
unittest.main()
テストおじさん: からの〜
$ python main_test.py
.....
----------------------------------------------------------------------
Ran 5 tests in 0.000s
OK
テストおじさん: 繰り返しの実行処理は・・・こんな感じかな。
@staticmethod
def iterate(count):
# 配列を宣言する
# 指定された回数だけ繰り返し実行する
# 配列に実行結果をセットする
# 配列を返す
return [1, 2, "Fizz", 4, "Buzz"]
テストおじさん: Pythonの繰り返し構文はfor in一択だから・・・(しれっとrangeを使う)。
@staticmethod
def iterate(count):
# 配列を宣言する
array = []
# 指定された回数だけ繰り返し実行する
for n in range(count):
# 配列に実行結果をセットする
array.append(n)
# 配列を返す
return array
テストおじさん: テストして・・・よし、数字が連続して出力されてるな。
$ python main_test.py
....F
======================================================================
FAIL: test_回数を5回実行すると配列を返すようにする (__main__.FizzBuzzTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "main_test.py", line 44, in test_回数を5回実行すると配列を返すようにする
self.assertEqual([1, 2, "Fizz", 4, "Buzz"], FizzBuzz.iterate(5))
AssertionError: Lists differ: [1, 2, 'Fizz', 4, 'Buzz'] != [0, 1, 2, 3, 4]
First differing element 0:
1
0
- [1, 2, 'Fizz', 4, 'Buzz']
+ [0, 1, 2, 3, 4]
----------------------------------------------------------------------
Ran 5 tests in 0.001s
FAILED (failures=1)
@staticmethod
def iterate(count):
# 配列を宣言する
array = []
# 指定された回数だけ繰り返し実行する
for n in range(count):
# 配列に実行結果をセットする
array.append(FizzBuzz.generate(n))
# 配列を返す
return array
テストおじさん: これでいいかな・・・ありゃ。
$ python main_test.py
....F
======================================================================
FAIL: test_回数を5回実行すると配列を返すようにする (__main__.FizzBuzzTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "main_test.py", line 44, in test_回数を5回実行すると配列を返すようにする
self.assertEqual([1, 2, "Fizz", 4, "Buzz"], FizzBuzz.iterate(5))
AssertionError: Lists differ: [1, 2, 'Fizz', 4, 'Buzz'] != ['FizzBuzz', 1, 2, 'Fizz', 4]
First differing element 0:
1
'FizzBuzz'
- [1, 2, 'Fizz', 4, 'Buzz']
+ ['FizzBuzz', 1, 2, 'Fizz', 4]
----------------------------------------------------------------------
Ran 5 tests in 0.001s
FAILED (failures=1)
テストおじさん: あー、generate
の引数の number
が0から始まってるからなのね。
@staticmethod
def iterate(count):
# 配列を宣言する
array = []
# 指定された回数だけ繰り返し実行する
for n in range(count):
# 配列に実行結果をセットする
array.append(FizzBuzz.generate(n + 1))
# 配列を返す
return array
テストおじさん: これでオッケー。
$ python main_test.py
.....
----------------------------------------------------------------------
Ran 5 tests in 0.000s
OK
テストおじさん: カバレッジを満たすテストケースを追加してと。
def test_回数を15回実行すると配列を返すようにする(self):
expect = [1, 2, "Fizz", 4, "Buzz", "Fizz", 7, 8, "Fizz", "Buzz", 11, "Fizz", 13, 14, "FizzBuzz"]
self.assertEqual(expect, FizzBuzz.iterate(15))
テストおじさん: これでオッケー。
$ python main_test.py
......
----------------------------------------------------------------------
Ran 6 tests in 0.000s
OK
2.3.5. アジャイルソフトウェア開発
テストおじさん: はい、これで仕様を満たすプログラムができました。
-
✓ 値が3ならばFizzを返すようにする
-
✓ 値が5ならばBuzzを返すようにする
-
✓ 値が15ならばFizzBuzzを返すようにする
-
✓ 複数回実行されたら結果を返すようにする
テストしてない人: 設計ドキュメントを書いて開発・テストという流れとはずいぶん違いますね。
テストおじさん: そうですね、いわゆるウォーターフォール型のアプローチだと最初にExcelとかでドキュメント書きますよね。
テストおじさん: 昔 IPO 書きましたよ。
解説しよう!テストおじさんはシステムエンジニアをやっていたのだ。
テストおじさん: ドキュメントがソフトウェアの変更と動機が取れていれば問題ないんですけどね。
テストおじさん: テストコードを見てください。
class FizzBuzzTest(unittest.TestCase):
def test_値が3ならばFizzを返すようにする(self):
self.assertEqual("Fizz", FizzBuzz.generate(3))
def test_値が5ならばBuzzを返すようにする(self):
self.assertEqual("Buzz", FizzBuzz.generate(5))
def test_値が15ならばFizzBuzzを返すようにする(self):
self.assertEqual("FizzBuzz", FizzBuzz.generate(15))
def test_値が101ならば101を返すようにする(self):
self.assertEqual(101, FizzBuzz.generate(101))
def test_回数を5回実行すると配列を返すようにする(self):
self.assertEqual([1, 2, "Fizz", 4, "Buzz"], FizzBuzz.iterate(5))
def test_回数を15回実行すると配列を返すようにする(self):
expect = [1, 2, "Fizz", 4, "Buzz", "Fizz", 7, 8, "Fizz", "Buzz", 11, "Fizz", 13, 14, "FizzBuzz"]
self.assertEqual(expect, FizzBuzz.iterate(15))
テストおじさん: これは仕様だから、こうしたほうがいいですね。
class FizzBuzzTest(unittest.TestCase):
def test_値が3ならばFizzを返す(self):
self.assertEqual("Fizz", FizzBuzz.generate(3))
def test_値が5ならばBuzzを返す(self):
self.assertEqual("Buzz", FizzBuzz.generate(5))
def test_値が15ならばFizzBuzzを返す(self):
self.assertEqual("FizzBuzz", FizzBuzz.generate(15))
def test_値が101ならば101を返す(self):
self.assertEqual(101, FizzBuzz.generate(101))
def test_回数を5回実行すると配列を返す(self):
self.assertEqual([1, 2, "Fizz", 4, "Buzz"], FizzBuzz.iterate(5))
def test_回数を10回実行すると配列を返す(self):
self.assertEqual([1, 2, "Fizz", 4, "Buzz", "Fizz", 7, 8, "Fizz", "Buzz"], FizzBuzz.iterate(10))
def test_回数を15回実行すると配列を返す(self):
expect = [1, 2, "Fizz", 4, "Buzz", "Fizz", 7, 8, "Fizz", "Buzz", 11, "Fizz", 13, 14, "FizzBuzz"]
self.assertEqual(expect, FizzBuzz.iterate(15))
テストおじさん: vオプションをつけてテストを実行したら。
$ python main_test.py -v
test_値が101ならば101を返す (__main__.FizzBuzzTest) ... ok
test_値が15ならばFizzBuzzを返す (__main__.FizzBuzzTest) ... ok
test_値が3ならばFizzを返す (__main__.FizzBuzzTest) ... ok
test_値が5ならばBuzzを返す (__main__.FizzBuzzTest) ... ok
test_回数を15回実行すると配列を返す (__main__.FizzBuzzTest) ... ok
test_回数を5回実行すると配列を返す (__main__.FizzBuzzTest) ... ok
----------------------------------------------------------------------
Ran 6 tests in 0.000s
OK
テストおじさん: これって実行できる設計ドキュメントみたいなものですよね。
テストおじさん: これまでの実装からもわかると思いますが従来のウォーターフォール型とは違う考え方がテスト駆動開発の根底にあります。
テストおじさん: その考え方は アジャイルソフトウェア開発宣言 から来ているんですよ。
私たちは、ソフトウェア開発の実践
あるいは実践を手助けをする活動を通じて、
よりよい開発方法を見つけだそうとしている。
この活動を通して、私たちは以下の価値に至った。
プロセスやツールよりも個人と対話を、
包括的なドキュメントよりも動くソフトウェアを、
契約交渉よりも顧客との協調を、
計画に従うことよりも変化への対応を、
価値とする。すなわち、左記のことがらに価値があることを
認めながらも、私たちは右記のことがらにより価値をおく。
テストおじさん: 最も有名なアジャイルメソッド(手法)のエクストリームプログラミング(XP)の主要プラクティスの一つが テストファーストプログラミング なんですよ。
テストファーストプログラミング(Tes-First Programming)
コードを変更する前に、失敗する自動テストを書くこと。
テストおじさん: 個人的には インクリメンタルな設計 も含めてテスト駆動開発かなと思ってます。
インクリメンタルな設計(Incremental Design)
システムの設計に毎日手を入れること。システムの設計は、その日のシステムのニーズにうまく合致させること。最適だと思われる設計が理解できなくなってきたら、少しずつだが着実に、自分の理解できる設計に戻していくこと。
この後、開発プロセスに関するディスカッションを行う。
2.4. リファクタリングから始めるテスト駆動開発
テストおじさん: 仕様を満たすコードを実装してテストもパスするようになりましたが・・・
テストおじさん: テストがパスしたら終わりではありません!
テスト書いてないとt_wadaさんの前で言った人: ?
テスト書きたい人: ?
テストしてない人: ?
テストおじさん: リファクタリング の始まりです。
リファクタリング(名詞):外部から見たときの振る舞いを保ちつつ、理解や修正が簡単になるように、ソフトウェアの内部構造を変化させること。
リファクタリングする(動詞):一連のリファクタリングを適用して、外部から見た振る舞いの変更なしに、ソフトウェアを再構築すること。
2.4.1. コードの不吉な臭い
テストおじさん: まずは コードの不吉な臭い を嗅ぎ取ります。
コードの不吉な臭い
重複したコード
長過ぎるコード
巨大なクラス
長すぎるパラメータリスト
変更の偏り
変更の分散
特性の横恋慕
データの群れ
基本データ型への執着
スイッチ文
パラレル継承
怠け者クラス
疑わしき一般化
一時的属性
メッセージの連鎖
仲介人
不適切な関係
クラスのインタフェース不一致
未成熟なクラスライブラリ
データクラス
相続拒否
コメント
テスト書いてないとt_wadaさんの前で言った人: 臭い?
テストおじさん: うーむ、なんというかコードを眺めていてこれはアレだなとかそんな感覚ですかね。
テストおじさん: 実際にコードを見ましょう。
2.4.2. コメント
class FizzBuzz:
@staticmethod
def generate(number):
value = number
if number % 3 == 0 and number % 5 == 0:
value = "FizzBuzz"
elif number % 3 == 0:
value = "Fizz"
elif number % 5 == 0:
value = "Buzz"
return value
@staticmethod
def iterate(count):
# 配列を宣言する
array = []
# 指定された回数だけ繰り返し実行する
for n in range(count):
# 配列に実行結果をセットする
array.append(FizzBuzz.generate(n + 1))
# 配列を返す
return array
テストおじさん: 自分が最初に臭いを感じたのが コメント ですね。
コメント
コメントを書いてはいけないなどと言うつもりはまったくありません。コメントは決して悪い臭いではなく、むしろいい香りなのです。ここでコメントについて言及しているのは、コメントが消臭剤として使われることがあるからです。コメントが非常に丁寧に書かれていたのは、実はわかりにくいコードを補うためだったということがよくあるのです。
テストおじさん: この場合は繰り返し処理を導出するための下書きコメントが冗長な消臭香をプンプン出してる感じですかね。
テストおじさん: こういうのを放置しておくと・・・
@staticmethod
def iterate(count):
# 配列を宣言する
array = []
# 指定された回数だけ繰り返し実行する
for n in range(count):
# 配列に実行結果をセットする
array.append(FizzBuzz.generate(n + 1))
# 配列を返す (1)
return str(array) (2)
テストおじさん: コメントは配列を返すとあるのに実際は文字列を返すというコメントバグの原因になります。
テストおじさん: 冗長なコメントは削除しましょう。
class FizzBuzz:
@staticmethod
def generate(number):
value = number
if number % 3 == 0 and number % 5 == 0:
value = "FizzBuzz"
elif number % 3 == 0:
value = "Fizz"
elif number % 5 == 0:
value = "Buzz"
return value
@staticmethod
def iterate(count):
array = []
for n in range(count):
array.append(FizzBuzz.generate(n + 1))
return array
テストおじさん: テストを実行して変更がプログラムを壊してないか確認します。
$ python main_test.py
......
----------------------------------------------------------------------
Ran 6 tests in 0.000s
OK
テストおじさん: はい、冗長なコメント削除リファクタリング完了。
2.4.3. 長すぎるメソッド
テストおじさん: 変更後のコードを眺めながら コードの不吉な臭い がする部分を探します。
class FizzBuzz:
@staticmethod
def generate(number):
value = number
if number % 3 == 0 and number % 5 == 0:
value = "FizzBuzz"
elif number % 3 == 0:
value = "Fizz"
elif number % 5 == 0:
value = "Buzz"
return value
テストおじさん: 次に自分が感じたのが 長すぎるメソッド ですね。
長すぎるメソッド
オブジェクト指向プログラムで、長く充実した人生を送るのは、常に、短いメソッドを持ったオブジェクトです。
テストおじさん: if-else文は仕様変更とともに条件分岐が複雑になっていく傾向があります。
テストおじさん: 今回は ガード節による入れ子条件記述の置き換え からガード節の導入を実施しましょう。
ガード節による入れ子条件記述の置き換え
メソッド内に正常ルートが不明確な条件つき振る舞いがある。
特殊ケースすべてに対してガード節を使う。
プロダクトコードを変更するテストおじさん。
class FizzBuzz:
@staticmethod
def generate(number):
value = number
if number % 3 == 0 and number % 5 == 0:
return "FizzBuzz"
if number % 3 == 0:
return "Fizz"
if number % 5 == 0:
return "Buzz"
return value
テストおじさん: テストを実行して変更がプログラムを壊してないか確認します。
$ python main_test.py
......
----------------------------------------------------------------------
Ran 6 tests in 0.000s
OK
テストおじさん: 一時変数 value
が冗長ですね削除しましょう。
class FizzBuzz:
@staticmethod
def generate(number):
if number % 3 == 0 and number % 5 == 0:
return "FizzBuzz"
if number % 3 == 0:
return "Fizz"
if number % 5 == 0:
return "Buzz"
return number
テストおじさん: 確認、確認。
$ python main_test.py
......
----------------------------------------------------------------------
Ran 6 tests in 0.000s
OK
テストおじさん: ガード節なんですがPythonは言語仕様上インデントしないといけないんですが言語によっては1行で書けます。
テストおじさん: 例えばRubyだとこう。
class FizzBuzz
def self.generate(number)
return 'FizzBuzz' if number % 3 == 0 and number % 5 == 0
return 'Fizz' if number % 3 == 0
return 'Buzz' if number % 5 == 0
return number
end
テストおじさん: 次は条件内容に注意を向けてみましょう。
テストおじさん: ここでは 条件記述の分解 を適用してみましょう。
条件記述の分解
複雑な条件(if-then-els)がある。
その条件記述部とthen部およびelse部から、メソッドを抽出する。
class FizzBuzz:
@staticmethod
def generate(number):
if FizzBuzz.can_divide_three_and_five(number):
return "FizzBuzz"
if number % 3 == 0:
return "Fizz"
if number % 5 == 0:
return "Buzz"
return number
@staticmethod
def can_divide_three_and_five(number):
return number % 3 == 0 and number % 5 == 0
テストおじさん: 壊れてませんよね。
$ python main_test.py
......
----------------------------------------------------------------------
Ran 6 tests in 0.000s
OK
テスト書いてないとt_wadaさんの前で言った人: ここは割り切れるなのでcan be dividedですね。
テストおじさん: 確かに表現が不適切ですね。
解説しよう!テスト書いてないとt_wadaさんの前で言った人は教育関係のお仕事に従事されているのだ。
class FizzBuzz:
@staticmethod
def generate(number):
if FizzBuzz.can_bd_divided_three_and_five(number):
return "FizzBuzz"
if number % 3 == 0:
return "Fizz"
if number % 5 == 0:
return "Buzz"
return number
@staticmethod
def can_be_divided_three_and_five(number):
return number % 3 == 0 and number % 5 == 0
テストおじさん: テスト、テスト。
$ python main_test.py
......
----------------------------------------------------------------------
Ran 6 tests in 0.000s
OK
テストおじさん: 残りの条件も変更しましょう。
class FizzBuzz:
@staticmethod
def generate(number):
if FizzBuzz.can_be_divided_three_and_five(number):
return "FizzBuzz"
if FizzBuzz.can_be_divided_three(number):
return "Fizz"
if FizzBuzz.can_be_divided_five(number):
return "Buzz"
return number
@staticmethod
def can_be_divided_three_and_five(number):
return number % 3 == 0 and number % 5 == 0
@staticmethod
def can_be_divided_three(number):
number % 3 == 0
@staticmethod
def can_be_divided_five(number):
number % 5 == 0
テストおじさん: 今回は少し横着をしてテストなしに2つコードを追加しました。
テストおじさん: ではまとめてテスト・・・ファッ!?
$ python main_test.py
..FFFF
======================================================================
FAIL: test_値が3ならばFizzを返す (__main__.FizzBuzzTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "main_test.py", line 40, in test_値が3ならばFizzを返す
self.assertEqual("Fizz", FizzBuzz.generate(3))
AssertionError: 'Fizz' != 3
======================================================================
FAIL: test_値が5ならばBuzzを返す (__main__.FizzBuzzTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "main_test.py", line 43, in test_値が5ならばBuzzを返す
self.assertEqual("Buzz", FizzBuzz.generate(5))
AssertionError: 'Buzz' != 5
======================================================================
FAIL: test_回数を10回実行すると配列を返す (__main__.FizzBuzzTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "main_test.py", line 55, in test_回数を10回実行すると配列を返す
self.assertEqual([1, 2, "Fizz", 4, "Buzz", "Fizz", 7, 8, "Fizz", "Buzz"], FizzBuzz.iterate(10))
AssertionError: Lists differ: [1, 2, 'Fizz', 4, 'Buzz', 'Fizz', 7, 8, 'Fizz', 'Buzz'] != [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
First differing element 2:
'Fizz'
3
- [1, 2, 'Fizz', 4, 'Buzz', 'Fizz', 7, 8, 'Fizz', 'Buzz']
+ [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
======================================================================
FAIL: test_回数を5回実行すると配列を返す (__main__.FizzBuzzTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "main_test.py", line 52, in test_回数を5回実行すると配列を返す
self.assertEqual([1, 2, "Fizz", 4, "Buzz"], FizzBuzz.iterate(5))
AssertionError: Lists differ: [1, 2, 'Fizz', 4, 'Buzz'] != [1, 2, 3, 4, 5]
First differing element 2:
'Fizz'
3
- [1, 2, 'Fizz', 4, 'Buzz']
+ [1, 2, 3, 4, 5]
----------------------------------------------------------------------
Ran 6 tests in 0.001s
FAILED (failures=4)
テスト書いてないとt_wadaさんの前で言った人: return
が書かれてませんね。
テストおじさん: こいつは失礼しました(´Д`)
テストおじさん: とまあ、しょぼいミスもテストが教えてくれるわけですね。
そしてコードを修正してテストを再実行するテストおじさん。
class FizzBuzz:
@staticmethod
def generate(number):
if FizzBuzz.can_be_divided_three_and_five(number):
return "FizzBuzz"
if FizzBuzz.can_be_divided_three(number):
return "Fizz"
if FizzBuzz.can_be_divided_five(number):
return "Buzz"
return number
@staticmethod
def can_be_divided_three_and_five(number):
return number % 3 == 0 and number % 5 == 0
@staticmethod
def can_be_divided_three(number):
return number % 3 == 0
@staticmethod
def can_be_divided_five(number):
return number % 5 == 0
$ python main_test.py
......
----------------------------------------------------------------------
Ran 6 tests in 0.000s
OK
テストおじさん: オッケー。
2.4.4. 3度目の法則
テストしてない人: リファクタリングをするタイミングはあるのですか?
テストおじさん: 3度目の法則 というのがあります。
3度目の法則
最初は、単純に作業を行います。2度目に以前と似たようなことをしていると気づいた場合は、重複や無駄を意識しつつも、とにかく作業を続けてかまいません。 そして3度目に同じようなことをしていると気づいたならば、そこでリファクタリングをするのです。
テストおじさん: あとは・・・
-
機能追加時にリファクタリングを行う
-
バグフィックスの時にリファクタリングを行う
-
コードレビューの時にリファクタリングを行う
テストおじさん: あたりですかね。
テストおじさん: リファクタリングのヒント も参考になりますね。
リファクタリングのヒント
❏ 構造的に機能を付け加えにくいプログラムに、新規機能を追加しなければならない場合には、まず機能追加が簡単になるようにリファクタリングをしてから追加を行うこと。
❏ リファクタリングに入る前に、しっかりとした一連のテスト群が用意できているかを確認すること。これらのテストには自己診断機能が不可欠である。
❏ リファクタリングでは小さなステップでプログラムを変更していく。そのため、誤ったことしても、バグを見つけるのは簡単である。
❏ コンパイラが理解出るコードは誰にでも書ける。すぐれたプログラマは、人間にとってわかりやすいコードを書く。
❏ リファクタリング(名詞):外側から見たときの振る舞いを保ちつつ、理解や修正が簡単になるように、ソフトウェアの内部構造を変化させること。
❏ リファクタリングする(動詞):一連のリファクタリングを適用して、外部から見た振る舞いの変更なしに、ソフトウェアを再構築すること。
❏ 3三度目になったらリファクタリング開始。
❏ あまり早期にインタフェースを公開しないこと。スムーズなリファクタリングのために、時にはコードの所有権のポリシーを変えることも必要。
❏ コメントの必要を感じたときにはリファクタリングを行って、コメントを書かなくとも内容がわかるようなコードを目指すこと。
❏ テストを完全に自動化して、その結果もテストにチェックさせること。
❏ テストをひとそろいにしておくと、バグの検出に絶大な威力を発揮する。これによって、バグの発見にかかる時間は削除される。
2.4.5. 名前重要
テスト書きたい人: 変数名やメソッド名が長くてもいいのですか?
テストおじさん: 意図が明確な名前にする ために 名前に情報を詰め込む 結果長い名前になるのは問題ないと思いますよ。
意図が明確な名前にする
その意図が明確な名前を付ける。いうだけなら簡単なことです。ここで、しっかりと胸に刻みつけておいて欲しいのは我々が名前付けを大変重要なことだと思っているということです。 よい名前を付けるのには時間がかかりますが、それによって、より多くの時間を節約できます。それ故、名前を付けるときには注意深く、そして後でよりよいものが思い浮かんだら、変更してください。 そうすることで、あなたのコードを読む人(あなた自身を含めて)を幸福にすることができます。
変数、関数、クラス名は、次の大命題に応える必要があります。なぜそれが存在するのか、何をするのか、どのように使用するのか。 もしも名前に解説が必要なら、その名前は、意図が明確とはいえません。
名前に情報を詰め込む
名前をつけるときには、それが変数であっても、関数であっても、クラスであっても、同じ原則を当てはめることができる。 名前は短いコメントだと思えばいい。短くてもいい名前をつければ、それだけ多くの情報を伝えることができる。
テストおじさん: ちなみにRubyのコードですが
class FizzBuzz
def self.generate(number)
return 'FizzBuzz' if can_be_divided_by_three_and_five(number)
return 'Fizz' if can_be_divided_by_three(number)
return 'Buzz' if can_be_divided_by_five(number)
return number
end
テストおじさん: FizzBuzz generate FizzBuzz if number can be divided by three and five. (FizzBuzzはもし3と5で割り切れるならFizzBuzzを返す)とそのまま読めませんか?
テストおじさん: 今日のPythonコードも日本語にするとこんな感じかな。
class FizzBuzz():
@staticmethod
def generate(数値):
if FizzBuzz.三と五で割り切れる(数値):
return "FizzBuzz"
elif FizzBuzz.三で割れる(数値):
return "Fizz"
elif FizzBuzz.五で割れる(数値):
return "Buzz"
return 数値
@staticmethod
def 三と五で割り切れる(数値):
return 数値 % 3 == 0 and 数値 % 5 == 0
@staticmethod
def 三で割れる(数値):
return 数値 % 3 == 0
@staticmethod
def 五で割れる(数値):
return 数値 % 5 == 0
テストおじさん: 非プログラマでも日本語が読めればなんとなくやってることわかるとも思うんですよね。
テストおじさん: 個人的に非プログラマがコード見てウッってなるのはアルファベットや数字の羅列に対するアレルギー反応みたいなもんじゃないかと思ってるんですけどね。
テストおじさん: まあ、プログラマが見てもウッってなるコードはあるわけですが・・・
テスト書いてないとt_wadaさんの前で言った人: 名前大事!
テストおじさん: そうっすね、Matzも 名前重要 と言ってますし。
テスト書きたい人: 表記のルールとかスタイルってあるんですか?
テストおじさん: 代表的なスタイルとしてPascal形式、Camel形式あとハンガリアン記法なんてのもあります。
テストおじさん: Pythonにはスタイルガイドがあるのでそれに従いましょう。
3. ふりかえり
テストおじさん: さて、そろそろ時間となりますので本日のふりかえりに入りましょう。
テストおじさん: まず、今日やったことですがFizzBuzzをお題に 型 の実演をして以下のコードを作成しました。
import unittest
class FizzBuzz:
@staticmethod
def generate(number):
if FizzBuzz.can_be_divided_three_and_five(number):
return "FizzBuzz"
if FizzBuzz.can_be_divided_three(number):
return "Fizz"
if FizzBuzz.can_be_divided_five(number):
return "Buzz"
return number
@staticmethod
def can_be_divided_three_and_five(number):
return number % 3 == 0 and number % 5 == 0
@staticmethod
def can_be_divided_three(number):
return number % 3 == 0
@staticmethod
def can_be_divided_five(number):
return number % 5 == 0
@staticmethod
def iterate(count):
array = []
for n in range(count):
array.append(FizzBuzz.generate(n + 1))
return array
class FizzBuzzTest(unittest.TestCase):
def test_値が3ならばFizzを返す(self):
self.assertEqual("Fizz", FizzBuzz.generate(3))
def test_値が5ならばBuzzを返す(self):
self.assertEqual("Buzz", FizzBuzz.generate(5))
def test_値が15ならばFizzBuzzを返す(self):
self.assertEqual("FizzBuzz", FizzBuzz.generate(15))
def test_値が101ならば101を返す(self):
self.assertEqual(101, FizzBuzz.generate(101))
def test_回数を5回実行すると配列を返す(self):
self.assertEqual([1, 2, "Fizz", 4, "Buzz"], FizzBuzz.iterate(5))
def test_回数を15回実行すると配列を返す(self):
expect = [1, 2, "Fizz", 4, "Buzz", "Fizz", 7, 8, "Fizz", "Buzz", 11, "Fizz", 13, 14, "FizzBuzz"]
self.assertEqual(expect, FizzBuzz.iterate(15))
if __name__ == "__main__":
unittest.main()
型
プログラミングの型というのは、プログラミングの問題を解くためにキーボードやマウスの動きの練習である。実際に問題を解くわけではない。解き方はすでにわかっている。問題を解きながら体の動きや意思決定の練習をするのである。
ここでも完全に限りなく近づくことが目標となる。脳や指に動きや反応を覚えさせるために、何度も練習するのだ。練習するうちに、自分の動きや解決策が少しづつ改善・効率化されることに気づくだろう。
型を使った練習は、ホットキーや操作のイデオムの学習に適している。TDDやCI(継続的インテグレーション)などの規律の学習にも優れた方法である。そして、最も重要なのは、よくある問題と解決策の組み合わせを潜在意識に植えつけることで、現実のプログラミングの問題解決方法がわかるようになるということだ。
武術家のようにプログラマは複数の型を知り、定期的に練習することで、記憶に残るようになる。型の多くは、http://katas.softwarecraftsmanship.org にある。
3.1. Keep
テストおじさん: テスト駆動を進めるにあたってまず、 TODOリスト を作成します。
テストおじさん: TODOリスト を作成したら次は、テスファースト アサートファースト を心がけてください。
テストおじさん: 次に実装の流れですが、 何を書くべきかわかっているときは、明白な実装 を行う。わからないときには 仮実装 を行う。まだ正しい実装が見えてこないなら、 三角測量 を行います。
テストおじさん: そして、常に ベイビーステップ で進めて行くことに留意してください。
ベイビーステップ(Baby Steps)
大きな変更は、大きなステップでやりたくなるものである。距離が長く、時間を書けずにそこまで行くとなれば、そうするしかないように思える。だが、重大な変更を一気に行うのは危険だ。変更を頼まれるのは人間である。変更は不安が伴う。不安を伴えば、人間は変更をすばやくやろうとする。
テストおじさん: 最後に、テスト駆動開発はテストではないということです、 大事なことなので二回言いました。
TDDは分析技法であり、設計技法であり、実際には開発のすべてのアクティビティを構造化する技法なのだ。
3.2. Problem
テストおじさん: 今回のセッションの問題点ですが・・・
テスト書いてないとt_wadaさんの前で言った人: スタティックメソッドに関する説明をするとインスタンスやクラスの概念の説明にまで展開してしまいましたね。
テストおじさん: オブジェクト指向に関することは今回の主要な関心事ではなかったし説明するなら構成を考えないといけませんよね。
テストおじさん: あと、今回は時間的に実演だけになってしまいましたが参加メンバーにも手を動かしてもらってわかるからできる状態を体感してほしいですね。
テスト書いてないとt_wadaさんの前で言った人: 一方がテストを書いてもう一方が実装するみたいな?
テストおじさん: そうですね、いわゆる 技 の実演です。
技
プログラマもこれと同じ練習ができる。 ピンポンゲーム を使うのだ。まず、2人で型または簡単な問題を選ぶ。次に、1人がユニットテストを書き、もう1人がテストを成功させる。そして、役割を交代する。
3.3. Try
テストおじさん: 今回のセッションの問題点を踏まえて・・・
テスト書いてないとt_wadaさんの前で言った人: クラスベースの実装ではなく関数から実装するアプローチなら前提となる概念理解の負担が減りそうですね。
テストおじさん: それな! 次回は関数ベースの手続き型からテスト駆動開発で進めてみましょうか。
テストおじさん: あと、技 の実演もしたいですね。
テストおじさん: そろそろ時間となりましたので本日のセッションを終了したいと思います。
テストおじさん: 皆さんお疲れ様でした。
テストおじさん以外のメンバー: ありがとうございました。