1. 概要

1.2. 会場

1.3. 参加メンバー

1.4. お題

  • FizzBuzz

  • FizzBuzz をクラスを使わずにテスト駆動で実装する。

1.5. 言語

  • Python 3.6

2. セッション

2.1. 前回までのあらすじ

前回のセッションでクラスベースだとオブジェクト指向に関する解説も必要になることがわかったテストおじさん。

そこで今回はクラスを使わずにテスト駆動開発をやってみることにしたのであった。

さてさてどうなることやら・・・

2.2. TODOリストから始めるアジャイルソフトウェア開発

テストおじさん: はい、第2回 Re:ゼロから始めるテスト駆動開発 はじめます、レポジトリは ここね。

リモートさん: 今日は家からうっすら参加です。ご飯を食べたりで適当に抜けています。

テスト鬼さん: よろしくお願いします。

テスト初めて書いたひと: よろしくお願いします。

テストおじさん: 今日はFizzBuzzをクラスを使わずにテスト駆動で実装してみたいと思います。

テストおじさん: 今回のお題の仕様です。

1 から 100 までの数をプリントするプログラムを書け。 ただし 3 の倍数のときは数の代わりに「Fizz」と、5 の倍数のときは「Buzz」とプリントし、3 と 5 両方の倍数の場合には「FizzBuzz」とプリントすること。

テストおじさん: 前回のセッションとの違いとして今回はコンソールに出力させるというのが大きな違いですかね。

2.2.1. TODOリスト

テストおじさん: 最初にまず TODOリスト の作成をしましょう。

TODOリスト

何をテストすべきだろうか----着手する前に、必要になりそうなテストをリストに書き出しておこう。

テストおじさん: 仕様からTODOリストを作成するわけですがここは スモール・イズ・ビューティフル の原則に従ってシンプルに進めていきましょう。

定理1: スモール・イズ・ビューティフル

プログラムを書く時は小さなものから初めて、それを小さなままに保っておく。 簡単なフィルタプログラムでも、グラフィックパッケージでも、巨大なデータベースを構築するときでも、同じく小さな実用的なプログラムにする。 一つの巨大なプログラムにしようとする誘惑に負けないで、シンプルさを追求する。

TODOリストを書き出すテストおじさん。

  • ❏ 1 から 100 まで数をプリントできるようにする。

  • ❏ 3 の倍数のときは数の代わりに「Fizz」をプリントできるようにする。

  • ❏ 5 の倍数のときは「Buzz」とプリントできるようにする。

  • ❏ 3 と 5 両方の倍数の場合には「FizzBuzz」とプリントできるようにする。

テストおじさん: まずは100回繰り返し処理をすることをどうやって実装するかを考えるとして条件に関しては一旦置いておきましょう。

テストおじさん: 一つのプログラムには一つのことをうまくやらせる の原則ですね。

定理2: 一つのプログラムには一つのことをうまくやらせる

最良のプログラムは、クストーのレイク・フライのように、生涯において一つのことをうまくやらせるプログラムだ。 プログラムはメモリにロードされて、所定の働きをし、次の単一機能のプログラムの実行のために道を譲る。 簡単に聞こえるが、ソフトウェア開発者たちが単一機能プログラムを作り上げることだけを追求し続けること、これがいかに難しいことかを知ったら驚くかもしれない。

2.2.2. アジャイルソフトウェア開発

テスト鬼さん: TODOリストの粒度はどれくらいが適切なんですかね?

テストおじさん: 自分はテストメソッドに対応するぐらいを目安に書いてますね。

テスト初めて書いたひと: 実際の開発だとTODOリストの粒度って立場によって変わってくると思うんですがどう取りまとめて行けばいいんですかね?

テストおじさん: そうですよね、作業者レベルのTODOもアプリケーションの要求から落とし込まないといけませよね。

テストおじさん: そういったものをどう取り扱うかは アジャイルソフトウェア開発手法 が参考になるかと思います。

テスト初めて書いたひと: アジャイル?

テストおじさん: えーと、例えば今日のお題の仕様ですが。

1 から 100 までの数をプリントするプログラムを書け。 ただし 3 の倍数のときは数の代わりに「Fizz」と、5 の倍数のときは「Buzz」とプリントし、3 と 5 両方の倍数の場合には「FizzBuzz」とプリントすること。

テストおじさん: これが

1 から 1000 までの数をプリントするプログラムを書け。 ただし 3 の倍数のときは数の代わりに「Fizz」と、5 の倍数のときは「Buzz」とプリントし、3 と 5 両方の倍数の場合には「FizzBuzz」とプリントすること。

テストおじさん: とか

1 から 1000 までの数をプリントするプログラムを書け。 ただし 3 の倍数のときは数の代わりに 「FizzFizzFizz」 と、5 の倍数のときは 「BuzzBuzzBuzz」 とプリントし、3 と 5 両方の倍数の場合には 「FizzBuzzFizzBuzzFizzBuzz」 とプリントすること。 ただし、上記の処理は月初のみに行うこと。

テストおじさん: からの

1 から 1000 までの数をプリントするプログラムを書け。 ただし 3 の倍数のときは数の代わりに 「FizzFizzFizz」 と、5 の倍数のときは 「BuzzBuzzBuzz」 とプリントし、3 と 5 両方の倍数の場合には 「FizzBuzzFizzBuzzFizzBuzz」 とプリントすること。 ただし、上記の処理は月初のみに行うこと。 上記の仕様は現状のビジネスモデルの変更に伴い無効とする。

テスト鬼さん: !

テスト初めて書いたひと: !!

テストおじさん: みたいに仕様が確定した後もコロコロ変わっていくと従来型の開発アプローチでは対応が難しいですよね。

テストおじさん: そういったビジネスとITの変化を背景に生まれたのが アジャイルソフトウェア開発宣言 です。

私たちは、ソフトウェア開発の実践

あるいは実践を手助けをする活動を通じて、

よりよい開発方法を見つけだそうとしている。

この活動を通して、私たちは以下の価値に至った。

プロセスやツールよりも個人と対話を、

包括的なドキュメントよりも動くソフトウェアを、

契約交渉よりも顧客との協調を、

計画に従うことよりも変化への対応を、

価値とする。すなわち、左記のことがらに価値があることを

認めながらも、私たちは右記のことがらにより価値をおく。

テストおじさん: そして アジャイル宣言の背後にある原則 を実現するための手法として XP(eXtreme Programming)スクラム があります。

私たちは以下の原則に従う:

顧客満足を最優先し、 価値のあるソフトウェアを早く継続的に提供します。

要求の変更はたとえ開発の後期であっても歓迎します。 変化を味方につけることによって、お客様の競争力を引き上げます。

動くソフトウェアを、2-3週間から2-3ヶ月という できるだけ短い時間間隔でリリースします。

ビジネス側の人と開発者は、プロジェクトを通して 日々一緒に働かなければなりません。

意欲に満ちた人々を集めてプロジェクトを構成します。 環境と支援を与え仕事が無事終わるまで彼らを信頼します。

情報を伝えるもっとも効率的で効果的な方法は フェイス・トゥ・フェイスで話をすることです。

動くソフトウェアこそが進捗の最も重要な尺度です。

アジャイル・プロセスは持続可能な開発を促進します。 一定のペースを継続的に維持できるようにしなければなりません。

技術的卓越性と優れた設計に対する 不断の注意が機敏さを高めます。

シンプルさ(ムダなく作れる量を最大限にすること)が本質です。

最良のアーキテクチャ・要求・設計は、 自己組織的なチームから生み出されます。

チームがもっと効率を高めることができるかを定期的に振り返り、 それに基づいて自分たちのやり方を最適に調整します。

XPとは何か

エクストリームプログラミング(XP)はソーシャルチェンジである。 XPとは、以前はうまくいいていたかもしれないが、今では最高の仕事の邪魔になっている習慣やパターンを手放すことだ。 XPとは、これまで自分たちを守ってきてくれたが、今では生産性の妨げになっているものを捨て去ることだ。 何だか自分がさらけ出されたような気持ちになるかもしれない。

スクラムとは何か

スクラムは、共同創始者の2人、すなわちJeff Sutherland と Ken Schwaber による共著「スクラムガイド」というコンパクトな文書により定義される、ソフトウェア開発プロセスです。

スクラムの定義

スクラム(名詞):複雑で変化の激しい問題に対応するためのフレームワークであり、可能な限り価値の高いプロダクトを生産的かつ創造的に届けるためのものである。

テストおじさん: 要求をソフトウェアに落とし込むためのプラクティスとして ユーザーストーリー というのがあります。

テストおじさん: ユーザーストーリー を管理する スクラム のプラクティスとして プロダクトバックログ スプリントバックログ というのがあります。

ユーザーストーリーは、顧客がソフトウェアで実現したいと思っているフィーチャを簡潔に記述したものだ。 通常、ユーザーストーリーは、あまり大きくないインデックスカードに書く(なんでもかんでも書き出そうとすることを物理的に制限するためだ)。 インデックスカードには簡潔にしか記述しないので、詳細は顧客のところへ出向いてじかに会話することを促進するという仕掛けになっているんだ。

プロダクバックログとは何か

プロダクトバックログは、プロダクトで実現したいことを優先順位をつけて一覧にしたものです。 プロダクトバックログには、機能の追加や修正、ユーザの要望などが含まれます。 プロダクトオーナーが、その内容と優先順位付けに責任を持ちます。

スプリントバックログとは何か

スプリントバックログは、スプリント期間内で行うと判断したプロダクトバックログアイテムと、それらプロダクトバックログアイテムを実現するためのタスクを俯瞰できるよう表したものです。 開発チームが、その内容に責任を持ちます。

テストおじさん: プログラムマ目線なら TODOリストスプリントバックログ でいうプロダクトバックログアイテムを実現するためのタスクあたりの粒度になるんじゃ何ですかね。

テストおじさん: 今回のお題をそれぞれのスタイルで記述すると。

ユーザーストーリー

  • <テスト駆動開発主催者>として

  • <シンプルなプログラムを題材にテスト駆動開発を体験して>欲しい

  • なぜなら<テスト駆動開発は体験しないと>わからないからだ

プロダクトバックログ

  • テスト駆動開発を体験するシンプルなプログラムを作成する

スプリントバックログアイテム

  • 以下の仕様のPythonプログラムをテスト駆動開発で実装する。なお、実装にあたってクラスは使用しない。

    1 から 100 までの数をプリントするプログラムを書け。
    ただし 3 の倍数のときは数の代わりに「Fizz」と、5 の倍数のときは「Buzz」とプリントし、3 と 5 両方の倍数の場合には「FizzBuzz」とプリントすること。

プロダクトバックログアイテムを実現するためのタスク

  • ❏ 1 から 100 まで数をプリントできるようにする。

  • ❏ 3 の倍数のときは数の代わりに「Fizz」をプリントできるようにする。

  • ❏ 5 の倍数のときは「Buzz」とプリントできるようにする。

  • ❏ 3 と 5 両方の倍数の場合には「FizzBuzz」とプリントできるようにする。

テスト初めて書いたひと: TODOリスト に抜けや漏れがあった場合はどうするんですかね?

要求の変更はたとえ開発の後期であっても歓迎します。 変化を味方につけることによって、お客様の競争力を引き上げます。

テストおじさん: の原則に従い、テストファーストプログラミングインクリメンタルな設計TODOリスト の抜けを埋めて漏れを塞ぎましょう。

2.3. テストファーストから始めるアーキテクチャ

2.3.1. テストファースト

テストおじさん: では1つ目の TODOリスト から片付けていきましょうか。

main_test.py ファイルを新規作成するテストおじさん。

テストおじさん: まずは テストファースト で。

$ python main_test.py -v

----------------------------------------------------------------------
Ran 0 tests in 0.000s

OK

テストファースト

いつテストを書くべきだろうか----それはテスト対象のコードを書く前だ。

2.3.2. アサートファースト

テストおじさん: さて、1から100までコンソールに出力させないといけませんがまずは アサートファースト仮実装 を行ってテストを通しておきましょう。

アサートファースト

いつアサーションを書くべきだろうか----最初に書こう

  • システム構築はどこから始めるべきだろうか。システム構築が終わったらこうなる、というストーリーを語るところからだ。

  • 機能はどこから書き始めるべきだろうか。コードが書き終わったらこのように動く、というテストを書くところからだ。

  • ではテストはどこから書き始めるべきだろうか。それはテストの終わりにパスすべきアサーションを書くところからだ。

仮実装を経て本実装へ

失敗するテストを書いてから、最初に行う実装はどのようなものだろうか----ベタ書きの値を返そう。

テストおじさん: はい、オッケー。

$ python main_test.py -v
test_1から100まで数をプリントできるようにする (__main__.MainTest) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

テスト鬼さん: -v のオプションは?

テストおじさん: これは実施するテストを表示するオプションです。

テストおじさん: オプションなしで実行すると。

$ python main_test.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

テストおじさん: こんな感じになります。

2.3.3. 仮実装

コードを書き換えてテストを実行するテストをおじさん。

def execute():
    print(1)
$ python main_test.py
1
F
======================================================================
FAIL: test_1から100まで数をプリントできるようにする (__main__.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "main_test.py", line 10, in test_1から100まで数をプリントできるようにする
    self.assertEqual("1", execute())
AssertionError: '1' != None

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (failures=1)

テストおじさん: 1をコンソールに出力はできたけど結果をどうやってアサーションしましょうかね。

テストおじさん: だいたいこんな時私は Python unittest 標準出力 みたいなキーワードでググります。

テストおじさん: いくつかの記事を眺めて良さげなのを試します。

テストおじさん: 今回は こちらのQiitaの記事 を参考にさせていただきましょう。

いくつかコードを追記するテストおじさん

import unittest
from test.support import captured_stdout


def execute():
    print(1)


class MainTest(unittest.TestCase):
    def test_1から100まで数をプリントできるようにする(self):
        with captured_stdout() as stdout:
            execute()
        self.assertEqual("1", stdout.getvalue())

テストおじさん: こうかな・・・テスト!

$ python main_test.py -v
test_1から100まで数をプリントできるようにする (__main__.MainTest) ... FAIL

======================================================================
FAIL: test_1から100まで数をプリントできるようにする (__main__.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "main_test.py", line 13, in test_1から100まで数をプリントできるようにする
    self.assertEqual("1", stdout.getvalue())
AssertionError: '1' != '1\n'
- 1
+ 1

?   +


----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (failures=1)

テストおじさん: ありゃ、えーと AssertionError: '1' != '1\n' ・・・ふむ、改行されて出力されるのね。

class MainTest(unittest.TestCase):
    def test_1から100まで数をプリントできるようにする(self):
        with captured_stdout() as stdout:
            execute()
        self.assertEqual("1\n", stdout.getvalue())

テストおじさん: これで・・・オッケー。

$ python main_test.py -v
test_1から100まで数をプリントできるようにする (__main__.MainTest) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

2.3.4. 三角測量

テストおじさん: さて、アサーションができようになったので while文 の制御構造を使って繰り返し実行の処理を実装してみましょう。

def execute():
    n = 100
    while n != 0:
        print(n)

テストおじさん: はい、実行・・・おや?

$ python main_test.py -v
test_1から100まで数をプリントできるようにする (__main__.MainTest) ...

テストおじさん: はい nがマイナスされないので n != 0 の条件になることができず無限ループに入ってますね。

def execute():
    n = 100
    while n != 0:
        print(n)
        n = n - 1

テストおじさん: これで、実行!

$ python main_test.py -v
test_1から100まで数をプリントできるようにする (__main__.MainTest) ... FAIL

======================================================================
FAIL: test_1から100まで数をプリントできるようにする (__main__.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "main_test.py", line 16, in test_1から100まで数をプリントできるようにする
    self.assertEqual("1\n", stdout.getvalue())
AssertionError: '1\n' != '100\n99\n98\n97\n96\n95\n94\n93\n92\n91\n9[346 chars]n1\n'
+ 100
+ 99
+ 98
+ 97
+ 96
+ 95
+ 94
+ 93
+ 92
+ 91
+ 90
+ 89
+ 88
+ 87
+ 86
+ 85
+ 84
+ 83
+ 82
+ 81
+ 80
+ 79
+ 78
+ 77
+ 76
+ 75
+ 74
+ 73
+ 72
+ 71
+ 70
+ 69
+ 68
+ 67
+ 66
+ 65
+ 64
+ 63
+ 62
+ 61
+ 60
+ 59
+ 58
+ 57
+ 56
+ 55
+ 54
+ 53
+ 52
+ 51
+ 50
+ 49
+ 48
+ 47
+ 46
+ 45
+ 44
+ 43
+ 42
+ 41
+ 40
+ 39
+ 38
+ 37
+ 36
+ 35
+ 34
+ 33
+ 32
+ 31
+ 30
+ 29
+ 28
+ 27
+ 26
+ 25
+ 24
+ 23
+ 22
+ 21
+ 20
+ 19
+ 18
+ 17
+ 16
+ 15
+ 14
+ 13
+ 12
+ 11
+ 10
+ 9
+ 8
+ 7
+ 6
+ 5
+ 4
+ 3
+ 2
  1


----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (failures=1)

テストおじさん: 100まで出力できたけど今度はアサーションをどうしましょうかね。

テストおじさん: 先程参考にした記事 に実行結果を リスト(list) に格納する方法が書いてあったので使わせていただきましょう。

class MainTest(unittest.TestCase):
    def test_1から100まで数をプリントできるようにする(self):
        with captured_stdout() as stdout:
            execute()
            lines = stdout.getvalue().splitlines()

        self.assertEqual("1\n", lines[1])

テストおじさん: これでどうかな?

$ python main_test.py -v
test_1から100まで数をプリントできるようにする (__main__.MainTest) ... FAIL

======================================================================
FAIL: test_1から100まで数をプリントできるようにする (__main__.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "main_test.py", line 18, in test_1から100まで数をプリントできるようにする
    self.assertEqual("1\n", lines[1])
AssertionError: '1\n' != '99'
- 1
+ 99

----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (failures=1)

テストおじさん: おや? AssertionError: '1\n' != '99' ・・・改行コードはいらないのね・・・99?

デバッガを実行して lines を確認するテストおじさん。

テストおじさん: あーそうか、100から出力されてるからね。

        self.assertEqual("1", lines[100])

テストおじさん: こうか!

$ python main_test.py -v
test_1から100まで数をプリントできるようにする (__main__.MainTest) ... ERROR

======================================================================
ERROR: test_1から100まで数をプリントできるようにする (__main__.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "main_test.py", line 18, in test_1から100まで数をプリントできるようにする
    self.assertEqual("1", lines[100])
IndexError: list index out of range

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (errors=1)

テストおじさん: ファッ!? IndexError: list index out of range ・・・あー リスト(list) は0から始まる仕様だったな。

テストおじさん: こうね。

        self.assertEqual("1", lines[99])

テストおじさん: オッケー!

$ python main_test.py -v
test_1から100まで数をプリントできるようにする (__main__.MainTest) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

テストおじさん: 三角測量 を実施して100が出力していることも確認しておきましょう。

三角測量

テストから最も慎重に一般化を引き出すやり方はどのようなものだろうか----2つ以上の例があるときだけ、一般化を行うようにしよう。

テストおじさん: 一番最初のリストのインデクスは0だから・・・

    def test_1から100まで数をプリントできるようにする(self):
        with captured_stdout() as stdout:
            execute()
            lines = stdout.getvalue().splitlines()

        self.assertEqual("1", lines[99])
        self.assertEqual("100", lines[0])

テストおじさん: はい、オッケー!

$ python main_test.py -v
test_1から100まで数をプリントできるようにする (__main__.MainTest) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

テストおじさん: TODOリスト 一つ目を片付けることができました。

  • ✓ 1 から 100 まで数をプリントできるようにする。

  • ❏ 3 の倍数のときは数の代わりに「Fizz」をプリントできるようにする。

  • ❏ 5 の倍数のときは「Buzz」とプリントできるようにする。

  • ❏ 3 と 5 両方の倍数の場合には「FizzBuzz」とプリントできるようにする。

2.3.5. 明白な実装

テストおじさん: さて、 二つ目の TODOリスト に取り掛かるとしましょうか。

テストコードを追加するテストおじさん。

    def test_3の倍数のときは数の代わりにFizzをプリントする(self):
        with captured_stdout() as stdout:
            execute()
            lines = stdout.getvalue().splitlines()
        self.assertEqual("Fizz", lines[97])

テストおじさん: テストは・・・失敗するね、オッケー。

$ python main_test.py -v
test_1から100まで数をプリントできるようにする (__main__.MainTest) ... ok
test_3の倍数のときは数の代わりにFizzをプリントする (__main__.MainTest) ... FAIL

======================================================================
FAIL: test_3の倍数のときは数の代わりにFizzをプリントする (__main__.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "main_test.py", line 25, in test_3の倍数のときは数の代わりにFizzをプリントする
    self.assertEqual("Fizz", lines[97])
AssertionError: 'Fizz' != '3'
- Fizz
+ 3


----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (failures=1)

テスト初めて書いたひと: テストが失敗するのにオッケーだと!?

ニヤリと笑うテストおじさん。

テストおじさん: お忘れなく・・・今やっていることはテストではなく設計だということ。

皮肉なことに、TDDはテスト技法ではない(Cunninghamの公案)。TDDは分析技法であり、設計技法であり、実際には開発のすべてのアクティビティを構造化する技法なのだ。

テストおじさん: さて、では設計を満たす振る舞いを 明白な実装 しましょう。

def execute():
    n = 100
    while n != 0:
        if n % 3 == 0:
            print("Fizz")
        print(n)
        n = n - 1

テストおじさん: えい!

$ python main_test.py -v
test_1から100まで数をプリントできるようにする (__main__.MainTest) ... FAIL
test_3の倍数のときは数の代わりにFizzをプリントする (__main__.MainTest) ... ok

======================================================================
FAIL: test_1から100まで数をプリントできるようにする (__main__.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "main_test.py", line 20, in test_1から100まで数をプリントできるようにする
    self.assertEqual("1", lines[99])
AssertionError: '1' != '26'
- 1
+ 26


----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (failures=1)

テストおじさん: おや、最初のテストが失敗してしまいました。

変更箇所を眺めるテストおじさん。

def execute():
    n = 100
    while n != 0:
        if n % 3 == 0:
            print("Fizz") (1)
        print(n) (2)
        n = n - 1

テストおじさん: あー、printが2回実行されてますね。

テストおじさん: ここはこうで・・・

def execute():
    n = 100
    while n != 0:
        if n % 3 == 0:
            print("Fizz")
        else:
            print(n)
        n = n - 1

テストおじさん: こう!

$ python main_test.py -v
test_1から100まで数をプリントできるようにする (__main__.MainTest) ... ok
test_3の倍数のときは数の代わりにFizzをプリントする (__main__.MainTest) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK

テストおじさん: TODOリスト 二つ目を片付けることができました。

  • ✓ 1 から 100 まで数をプリントできるようにする。

  • ✓ 3 の倍数のときは数の代わりに「Fizz」をプリントできるようにする。

  • ❏ 5 の倍数のときは「Buzz」とプリントできるようにする。

  • ❏ 3 と 5 両方の倍数の場合には「FizzBuzz」とプリントできるようにする。

テストおじさん: さて、 三つ目の TODOリスト に取り掛かるとしましょうか。

テストおじさん: まずはテスト・・・もとい、設計でしたね。

    def test_5の倍数のときはBuzzとプリントする(self):
        with captured_stdout() as stdout:
            execute()
            lines = stdout.getvalue().splitlines()
        self.assertEqual("Buzz", lines[95])

テストおじさん: テストは・・・失敗すると。

$ python main_test.py -v
test_1から100まで数をプリントできるようにする (__main__.MainTest) ... ok
test_3の倍数のときは数の代わりにFizzをプリントする (__main__.MainTest) ... ok
test_5の倍数のときはBuzzとプリントする (__main__.MainTest) ... FAIL

======================================================================
FAIL: test_5の倍数のときはBuzzとプリントする (__main__.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "main_test.py", line 34, in test_5の倍数のときはBuzzとプリントする
    self.assertEqual("Buzz", lines[95])
AssertionError: 'Buzz' != '5'
- Buzz
+ 5


----------------------------------------------------------------------
Ran 3 tests in 0.001s

FAILED (failures=1)

テストおじさん: 5の場合はBuzzをプリントするから。

def execute():
    n = 100
    while n != 0:
        if n % 3 == 0:
            print("Fizz")
        elif n % 5 == 0:
            print("Buzz")
        else:
            print(n)
        n = n - 1

テストおじさん: これでどうかな?

$ python main_test.py -v
test_1から100まで数をプリントできるようにする (__main__.MainTest) ... FAIL
test_3の倍数のときは数の代わりにFizzをプリントする (__main__.MainTest) ... ok
test_5の倍数のときはBuzzとプリントする (__main__.MainTest) ... ok

======================================================================
FAIL: test_1から100まで数をプリントできるようにする (__main__.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "main_test.py", line 24, in test_1から100まで数をプリントできるようにする
    self.assertEqual("100", lines[0])
AssertionError: '100' != 'Buzz'
- 100
+ Buzz


----------------------------------------------------------------------
Ran 3 tests in 0.001s

FAILED (failures=1)

テストおじさん: 最初のテストが失敗してますね。えーと、 AssertionError: '100' != 'Buzz' ・・・

テストおじさん: 100は5で割り切れるからBuzzを返すのが正しい仕様ですね、修正しましょう。

    def test_1から100まで数をプリントできるようにする(self):
        with captured_stdout() as stdout:
            execute()
            lines = stdout.getvalue().splitlines()

        self.assertEqual("1", lines[99])
        self.assertEqual("Buzz", lines[0]) (1)

テストおじさん: はい、オッケーっと。

$ python main_test.py -v
test_1から100まで数をプリントできるようにする (__main__.MainTest) ... ok
test_3の倍数のときは数の代わりにFizzをプリントする (__main__.MainTest) ... ok
test_5の倍数のときはBuzzとプリントする (__main__.MainTest) ... ok

----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK

テストおじさん: TODOリスト 三つ目を片付けることができました。

  • ✓ 1 から 100 まで数をプリントできるようにする。

  • ✓ 3 の倍数のときは数の代わりに「Fizz」をプリントできるようにする。

  • ✓ 5 の倍数のときは「Buzz」とプリントできるようにする。

  • ❏ 3 と 5 両方の倍数の場合には「FizzBuzz」とプリントできるようにする。

テストおじさん: さて、 最後の TODOリスト に取り掛かるとしましょうか。

テストおじさん: まずは、テスコードという名の設計をしてと。

    def test_3と5両方の倍数の場合にはFizzBuzzとプリントする(self):
        with captured_stdout() as stdout:
            execute()
            lines = stdout.getvalue().splitlines()
        self.assertEqual("FizzBuzz", lines[85])

テストおじさん: 確認。

$ python main_test.py -v
test_1から100まで数をプリントできるようにする (__main__.MainTest) ... ok
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (__main__.MainTest) ... FAIL
test_3の倍数のときは数の代わりにFizzをプリントする (__main__.MainTest) ... ok
test_5の倍数のときはBuzzとプリントする (__main__.MainTest) ... ok

======================================================================
FAIL: test_3と5両方の倍数の場合にはFizzBuzzとプリントする (__main__.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "main_test.py", line 42, in test_3と5両方の倍数の場合にはFizzBuzzとプリントする
    self.assertEqual("FizzBuzz", lines[85])
AssertionError: 'FizzBuzz' != 'Fizz'
- FizzBuzz
+ Fizz


----------------------------------------------------------------------
Ran 4 tests in 0.001s

FAILED (failures=1)

テストおじさん: 3で割り切れてかつ5で割り切れるから。

def execute():
    n = 100
    while n != 0:
        if n % 3 == 0:
            print("Fizz")
        elif n % 5 == 0:
            print("Buzz")
        elif n % 3 == 0 and n % 5 == 0: (1)
            print("FizzBuzz")
        else:
            print(n)
        n = n - 1

テストおじさん: どうなるかな?

$ python main_test.py -v
test_1から100まで数をプリントできるようにする (__main__.MainTest) ... ok
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (__main__.MainTest) ... FAIL
test_3の倍数のときは数の代わりにFizzをプリントする (__main__.MainTest) ... ok
test_5の倍数のときはBuzzとプリントする (__main__.MainTest) ... ok

======================================================================
FAIL: test_3と5両方の倍数の場合にはFizzBuzzとプリントする (__main__.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "main_test.py", line 44, in test_3と5両方の倍数の場合にはFizzBuzzとプリントする
    self.assertEqual("FizzBuzz", lines[85])
AssertionError: 'FizzBuzz' != 'Fizz'
- FizzBuzz
+ Fizz


----------------------------------------------------------------------
Ran 4 tests in 0.001s

FAILED (failures=1)

テストおじさん: ありゃ? AssertionError: 'FizzBuzz' != 'Fizz' 期待した値と違うな。

該当コードにブレークポイントを設定したデバッガを起動するテストおじさん。

def execute():
    n = 100
    while n != 0:
        if n % 3 == 0:
            print("Fizz")
        elif n % 5 == 0:
            print("Buzz")
        elif n % 3 == 0 and n % 5 == 0: (1)
            print("FizzBuzz")
        else:
            print(n)
        n = n - 1

テストおじさん: 100から評価されるのか・・・15までステップ実行のめんどくさいな(´Д`)ハァ…

テストおじさん: 90も3と5で割り切れるよな。

def execute():
    n = 100
    while n != 0:
        if n % 3 == 0: (2)
            print("Fizz")
        elif n % 5 == 0:
            print("Buzz")
        elif n % 3 == 0 and n % 5 == 0: (1)
            print("FizzBuzz")
        else:
            print(n)
        n = n - 1

テストおじさん: あー90なら3で割り切れるからFizzを先に返すわな。

テストおじさん: ここはこうで・・・

def execute():
    n = 100
    while n != 0:
        if n % 3 == 0 and n % 5 == 0: (3)
            print("FizzBuzz")
        elif n % 3 == 0: (4)
            print("Fizz")
        elif n % 5 == 0:
            print("Buzz")
        else:
            print(n)
        n = n - 1

テストおじさん: こう!

$ python main_test.py -v
test_1から100まで数をプリントできるようにする (__main__.MainTest) ... ok
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (__main__.MainTest) ... ok
test_3の倍数のときは数の代わりにFizzをプリントする (__main__.MainTest) ... ok
test_5の倍数のときはBuzzとプリントする (__main__.MainTest) ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.001s

OK

テストおじさん: 明白な実装 により最後の TODOリスト も片付けることができました。

  • ✓ 1 から 100 まで数をプリントできるようにする。

  • ✓ 3 の倍数のときは数の代わりに「Fizz」をプリントできるようにする。

  • ✓ 5 の倍数のときは「Buzz」とプリントできるようにする。

  • ✓ 3 と 5 両方の倍数の場合には「FizzBuzz」とプリントできるようにする。

明白な実装

シンプルな操作を実現するにはどうすればいいだろうか----そのまま実装しよう。

仮実装や三角測量は、細かく細かく刻んだ小さなステップだ。だが、ときには実装をどうすべきか既に見えていることが。 そのまま進もう。例えば先ほどのplusメソッドくらいシンプルなものを仮実装する必要が本当にあるだろうか。 普通は、その必要はない。頭に浮かんだ明白な実装をただ単にコードに落とすだけだ。もしもレッドバーが出て驚いたら、あらためてもう少し歩幅を小さくしよう。

2.3.6. アーキテクチャ

テスト初めて書いたひと: 実際のアプリケーションだといろいろなプログラムや外部モジュールやサービスと連携されてますけど全部同じようにテストするんですか?

テストおじさん: 確かに全部同じように実施するのは現実的ではありませんよね。

テストおじさん: 製品アーキテクチャ というのがあります。

製品アーキテクチャとは、普通に訳すと「基本設計思想」ですが、それを経営戦略に応用すると、 「どのようにして製品を構成部品の単位に分解し、そこに製品機能を配分し、それによって必要となる部品間のインターフェース(つなぎ目)をいかに設計・調整するか」を意味します。

代表的な分類として、「モジュール型」「インテグラル型」があり、また「クローズ型」と「オープン型」に分けられます。

テストおじさん: 構成部品が今書いているプログラムに該当しますかね。

テストおじさん: 構成部品はテスト駆動で作り込みます。

テストおじさん: 部品間のインターフェース(つなぎ目)の設計もテスト駆動が適用できます。

テスト鬼さん: AWSのS3サービスと連携させる場合は自動テスト実行時に毎回呼び出したりするんですか?

テストおじさん: 独立したサービスの連携をテストする場合には Mock Object(偽装オブジェクト)パターン を使って振る舞いを差し替えたります。

Mock Object(偽装オブジェクト)パターン

構築処理が重かったり、準備に手間がかかったりするようなリソースに依存したオブジェクトをテストするにはどうすればよいだろうか----決められた結果を返す、偽物のオブジェクトを代わりに作成しよう。

テスト初めて書いたひと: スタブやドライバと言うやつですね。

テストおじさん: そうですね、TDD界隈では以下に分類されているそうです。

diag 8492495552876534393d7ba7fa1b749e

テストおじさん: 製品アーキテクチャ というのは主に製造業の分野の考えですがソフトウェアにもあります。

テストおじさん: そのソフトウェア分野でアーキテクチャに関して エンタープライズアプリケーションアーキテクチャパターン ではこう言及されてます。

アーキテクチャ

アーキテクチャは、多くの人が定義しようとして、なかなか同意に至らない用語である。 この用語には誰もが認める2つの要素があるという。1つは、システムから個々のパーツへとどこまでもブレークダウンできるということ、もう1つは、簡単には変更できない決定事項だということである。 また、次第に理解されてきたことだが、システムのアーキテクチャのあり方は1つだけでなく、1つのシステムには複数のアーキテクチャがあり、アーキテクチャにとって重要なことはシステムの存続期間の中で変わることがある。

テストおじさん: 私はいわゆる エンタープライズアプリケーション 分野の人です。

エンタープライズアプリケーション

エンタープライズアプリケーションには、給与計算、診療記録、出荷管理、コスト分析、信用調査、保険、サプライチェーン、会計、顧客サービス、外国為替取引などが含まれる。 一方、自動車用燃料噴射、ワープロ、エレベータ制御、化学プラント制御、化学プラント制御、電話交換、OS,コンパイラ、ゲームはエンタープライズアプリケーションに含まれない。

テストおじさん: エンタープライズアプリケーション の特徴として以下の物があります。

  • 永続データを伴う。

  • たくさんのデータを扱う。

  • 多くのユーザが同時にデータにアクセスする。

  • 多くのユーザインターフェース画面がある。

  • 他のエンタープライズアプリケーションと統合する必要がある。

テストおじさん: その エンタープライズアプリケーション をWebベースで作るときにはWebアプリケーションフレームワークを使うことでテストする範囲を狭めることができます。

テストおじさん: Webプレゼンテーションパターンの モデルビューコントローラ を実装したWebアプリケーションフレームワークに Ruby on RailsDjango があります。

モデルビューコントローラ

ユーザインターフェースの相互作用を3つの明確な役割へ分割する。

テストおじさん: こうしたWebアプリケーションを導入することで業務ロジックに集中してテスト駆動開発を進めることができます。

テストおじさん: まあ、フレームワークに習熟しないといけないし癖もありますけどね・・・

2.4. リファクタリングから始める構造化プログラミング

2.4.1. 重複したコード

テストおじさん: さて、仕様を満たすプログラムはできましたがまだ完了ではありません。

テストおじさん: リファクタリングの時間です!

テスト初めて書いたひと: コードの不吉な臭い ですね。

コードの不吉な臭い

  • 重複したコード

  • 長過ぎるコード

  • 巨大なクラス

  • 長すぎるパラメータリスト

  • 変更の偏り

  • 変更の分散

  • 特性の横恋慕

  • データの群れ

  • 基本データ型への執着

  • スイッチ文

  • パラレル継承

  • 怠け者クラス

  • 疑わしき一般化

  • 一時的属性

  • メッセージの連鎖

  • 仲介人

  • 不適切な関係

  • クラスのインタフェース不一致

  • 未成熟なクラスライブラリ

  • データクラス

  • 相続拒否

  • コメント

テストおじさん: イエス! その前に リファクタリングのヒント からチェックしてみましょう。

リファクタリングのヒント

  • 構造的に機能を付け加えにくいプログラムに、新規機能を追加しなければならない場合には、まず機能追加が簡単になるようにリファクタリングをしてから追加を行うこと。

  • リファクタリングに入る前に、しっかりとした一連のテスト群が用意できているかを確認すること。これらのテストには自己診断機能が不可欠である。

  • リファクタリングでは小さなステップでプログラムを変更していく。そのため、誤ったことしても、バグを見つけるのは簡単である。

  • コンパイラが理解出るコードは誰にでも書ける。すぐれたプログラマは、人間にとってわかりやすいコードを書く。

  • リファクタリング(名詞):外側から見たときの振る舞いを保ちつつ、理解や修正が簡単になるように、ソフトウェアの内部構造を変化させること。

  • リファクタリングする(動詞):一連のリファクタリングを適用して、外部から見た振る舞いの変更なしに、ソフトウェアを再構築すること。

  • 3三度目になったらリファクタリング開始。

  • あまり早期にインタフェースを公開しないこと。スムーズなリファクタリングのために、時にはコードの所有権のポリシーを変えることも必要。

  • コメントの必要を感じたときにはリファクタリングを行って、コメントを書かなくとも内容がわかるようなコードを目指すこと。

  • テストを完全に自動化して、その結果もテストにチェックさせること。

  • テストをひとそろいにしておくと、バグの検出に絶大な威力を発揮する。これによって、バグの発見にかかる時間は削除される。

テストおじさん: チェックを入れていきましょう。

テストおじさん: まず、リファクタリングに入る前の条件は満たしていますか?

  • ❏ 構造的に機能を付け加えにくいプログラムに、新規機能を追加しなければならない場合には、まず機能追加が簡単になるようにリファクタリングをしてから追加を行うこと。

  • リファクタリングに入る前に、しっかりとした一連のテスト群が用意できているかを確認すること。これらのテストには自己診断機能が不可欠である。

  • ❏ リファクタリングでは小さなステップでプログラムを変更していく。そのため、誤ったことしても、バグを見つけるのは簡単である。

  • ❏ コンパイラが理解出るコードは誰にでも書ける。すぐれたプログラマは、人間にとってわかりやすいコードを書く。

  • ❏ リファクタリング(名詞):外側から見たときの振る舞いを保ちつつ、理解や修正が簡単になるように、ソフトウェアの内部構造を変化させること。

  • ❏ リファクタリングする(動詞):一連のリファクタリングを適用して、外部から見た振る舞いの変更なしに、ソフトウェアを再構築すること。

  • ❏ 3三度目になったらリファクタリング開始。

  • ❏ あまり早期にインタフェースを公開しないこと。スムーズなリファクタリングのために、時にはコードの所有権のポリシーを変えることも必要。

  • ❏ コメントの必要を感じたときにはリファクタリングを行って、コメントを書かなくとも内容がわかるようなコードを目指すこと。

  • ❏ テストを完全に自動化して、その結果もテストにチェックさせること。

  • ❏ テストをひとそろいにしておくと、バグの検出に絶大な威力を発揮する。これによって、バグの発見にかかる時間は削除される。

テストおじさん: テストは用意できていますね、ではリファクタリングに入りましょう。

コードを眺めるテストおじさん。

class MainTest(unittest.TestCase):
    def test_1から100まで数をプリントできるようにする(self):
        with captured_stdout() as stdout: (1)
            execute()
            lines = stdout.getvalue().splitlines()

        self.assertEqual("1", lines[99])
        self.assertEqual("Buzz", lines[0])

    def test_3の倍数のときは数の代わりにFizzをプリントする(self):
        with captured_stdout() as stdout: (2)
            execute()
            lines = stdout.getvalue().splitlines()
        self.assertEqual("Fizz", lines[97])

    def test_5の倍数のときはBuzzとプリントする(self):
        with captured_stdout() as stdout: (3)
            execute()
            lines = stdout.getvalue().splitlines()
        self.assertEqual("Buzz", lines[95])

    def test_3と5両方の倍数の場合にはFizzBuzzとプリントする(self):
        with captured_stdout() as stdout: (4)
            execute()
            lines = stdout.getvalue().splitlines()
        self.assertEqual("FizzBuzz", lines[85])

テストおじさん: テストコードに 重複したコード がありますね。

重複したコード

はえある第1位は、「重複したコード」です。同じようなコードが2か所以上で見られたら、1か所にまとめることを考えると良いプログラムになります。

テストおじさん: メソッドの抽出 を実施して フィクスチャー にまとめましょう。

メソッドの抽出

ひとまとめにできるコードの断片がある。

コードの断片をメソッドにして、それに目的を表すような名前をつける。

フィクスチャー

複数のテストから使われる共通のオブジェクトを作るにはどうしたらよいだろうか---テストメソッド内のローカル変数をインスタンス変数に引き上げ、オーバーライドしたsetUpメソッドの中で初期化を行う。

class MainTest(unittest.TestCase):
    def setUp(self): (1)
        with captured_stdout() as stdout:
            execute()
            self.lines = stdout.getvalue().splitlines()

    def test_1から100まで数をプリントできるようにする(self):
        self.assertEqual("1", self.lines[99])
        self.assertEqual("Buzz", self.lines[0])

    def test_3の倍数のときは数の代わりにFizzをプリントする(self):
        self.assertEqual("Fizz", self.lines[97])

    def test_5の倍数のときはBuzzとプリントする(self):
        self.assertEqual("Buzz", self.lines[95])

    def test_3と5両方の倍数の場合にはFizzBuzzとプリントする(self):
        self.assertEqual("FizzBuzz", self.lines[85])

テストおじさん: プログラムが壊れてないか確認しましょう。

$ python main_test.py -v
test_1から100まで数をプリントできるようにする (__main__.MainTest) ... ok
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (__main__.MainTest) ... ok
test_3の倍数のときは数の代わりにFizzをプリントする (__main__.MainTest) ... ok
test_5の倍数のときはBuzzとプリントする (__main__.MainTest) ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.001s

OK

テストおじさん: 大丈夫ですね。

  • ❏ 構造的に機能を付け加えにくいプログラムに、新規機能を追加しなければならない場合には、まず機能追加が簡単になるようにリファクタリングをしてから追加を行うこと。

  • ✓ リファクタリングに入る前に、しっかりとした一連のテスト群が用意できているかを確認すること。これらのテストには自己診断機能が不可欠である。

  • ❏ リファクタリングでは小さなステップでプログラムを変更していく。そのため、誤ったことしても、バグを見つけるのは簡単である。

  • ❏ コンパイラが理解出るコードは誰にでも書ける。すぐれたプログラマは、人間にとってわかりやすいコードを書く。

  • ❏ リファクタリング(名詞):外側から見たときの振る舞いを保ちつつ、理解や修正が簡単になるように、ソフトウェアの内部構造を変化させること。

  • ❏ リファクタリングする(動詞):一連のリファクタリングを適用して、外部から見た振る舞いの変更なしに、ソフトウェアを再構築すること。

  • 3三度目になったらリファクタリング開始。

  • ❏ あまり早期にインタフェースを公開しないこと。スムーズなリファクタリングのために、時にはコードの所有権のポリシーを変えることも必要。

  • ❏ コメントの必要を感じたときにはリファクタリングを行って、コメントを書かなくとも内容がわかるようなコードを目指すこと。

  • ❏ テストを完全に自動化して、その結果もテストにチェックさせること。

  • ❏ テストをひとそろいにしておくと、バグの検出に絶大な威力を発揮する。これによって、バグの発見にかかる時間は削除される。

2.4.2. ベイビーステップ

テストおじさん: 続いてプロダクトコードに関してはどうですかね。

コードを眺めるテストおじさん。

def execute():
    n = 100
    while n != 0: (1)
        if n % 3 == 0:
            print("Fizz")
        elif n % 5 == 0:
            print("Buzz")
        elif n % 3 == 0 and n % 5 == 0:
            print("FizzBuzz")
        else:
            print(n)
        n = n - 1

テストおじさん: 1から100までをプリントするのに100から処理を開始していますね。

    def test_1から100まで数をプリントできるようにする(self):
        self.assertEqual("1", self.lines[99])
        self.assertEqual("Buzz", self.lines[0])

テストおじさん: 結果をリストに保存していますが1に対応するリストのインデクスが99って分かりづらいですよね。

テスト初めて書いたひと: たしかにそうですね。

テストおじさん: こういう時はまずこうしてみましょう。

def execute():
    n = 1
    while n != 100: (1)
        if n % 3 == 0 and n % 5 == 0:
            print("FizzBuzz")
        elif n % 3 == 0:
            print("Fizz")
        elif n % 5 == 0:
            print("Buzz")
        else:
            print(n)
        n = n - 1

テストおじさん: はい、確認・・・ありゃ?

$ python main_test.py -v
test_1から100まで数をプリントできるようにする (__main__.MainTest) ...

テストを Ctr-c で強制停止した後、デバッガを起動してステップ実行するテストおじさん。

テストおじさん: あー n = n - 1 では n != 100 の条件に到達することは永久にないっすね。

def execute():
    n = 1
    while n != 100: (1)
        if n % 3 == 0 and n % 5 == 0:
            print("FizzBuzz")
        elif n % 3 == 0:
            print("Fizz")
        elif n % 5 == 0:
            print("Buzz")
        else:
            print(n)
        n = n - 1 (2)

テストおじさん: ここはこうかな。

def execute():
    n = 1
    while n != 100:
        if n % 3 == 0 and n % 5 == 0:
            print("FizzBuzz")
        elif n % 3 == 0:
            print("Fizz")
        elif n % 5 == 0:
            print("Buzz")
        else:
            print(n)
        n = n + 1

テストおじさん: テスト!・・・ファッ!!

$ python main_test.py -v
test_1から100まで数をプリントできるようにする (__main__.MainTest) ... ERROR
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (__main__.MainTest) ... FAIL
test_3の倍数のときは数の代わりにFizzをプリントする (__main__.MainTest) ... FAIL
test_5の倍数のときはBuzzとプリントする (__main__.MainTest) ... FAIL

======================================================================
ERROR: test_1から100まで数をプリントできるようにする (__main__.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "main_test.py", line 26, in test_1から100まで数をプリントできるようにする
    self.assertEqual("1", self.lines[99])
IndexError: list index out of range

======================================================================
FAIL: test_3と5両方の倍数の場合にはFizzBuzzとプリントする (__main__.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "main_test.py", line 36, in test_3と5両方の倍数の場合にはFizzBuzzとプリントする
    self.assertEqual("FizzBuzz", self.lines[85])
AssertionError: 'FizzBuzz' != '86'
- FizzBuzz
+ 86


======================================================================
FAIL: test_3の倍数のときは数の代わりにFizzをプリントする (__main__.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "main_test.py", line 30, in test_3の倍数のときは数の代わりにFizzをプリントする
    self.assertEqual("Fizz", self.lines[97])
AssertionError: 'Fizz' != '98'
- Fizz
+ 98


======================================================================
FAIL: test_5の倍数のときはBuzzとプリントする (__main__.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "main_test.py", line 33, in test_5の倍数のときはBuzzとプリントする
    self.assertEqual("Buzz", self.lines[95])
AssertionError: 'Buzz' != 'Fizz'
- Buzz
+ Fizz


----------------------------------------------------------------------
Ran 4 tests in 0.001s

FAILED (failures=3, errors=1)

テストおじさん: テストが全部失敗してしまいましたね。

テストおじさん: こういう時は落ち着いて一つづつ確認していきましょう。

$ python -m unittest main_test.MainTest.test_1から100まで数をプリントできるようにする
E
======================================================================
ERROR: test_1から100まで数をプリントできるようにする (main_test.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/k2works/Projects/hiroshima-arc/re_zero_tdd/dev/20181116/replay/main_test.py", line 26, in test_1から100まで数をプリントできるようにする
    self.assertEqual("1", self.lines[99])
IndexError: list index out of range

----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (errors=1)

テストおじさん: IndexError: list index out of range リストの100番目に値が存在しないってこと?

コードを眺めるテストおじさん。

def execute():
    n = 1
    while n != 100: (1)
        if n % 3 == 0 and n % 5 == 0:
            print("FizzBuzz")
        elif n % 3 == 0:
            print("Fizz")
        elif n % 5 == 0:
            print("Buzz")
        else:
            print(n)
        n = n + 1

テストおじさん: あー while n != 100 だと100は含まれないわな。

テストおじさん: だから、ここはこうね。

def execute():
    n = 1
    while n != 101: (2)
        if n % 3 == 0 and n % 5 == 0:
            print("FizzBuzz")
        elif n % 3 == 0:
            print("Fizz")
        elif n % 5 == 0:
            print("Buzz")
        else:
            print(n)
        n = n + 1

テストおじさん: こうするとどうかな?

$ python -m unittest main_test.MainTest.test_1から100まで数をプリントできるようにする
F
======================================================================
FAIL: test_1から100まで数をプリントできるようにする (main_test.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/k2works/Projects/hiroshima-arc/re_zero_tdd/dev/20181116/replay/main_test.py", line 26, in test_1から100まで数をプリントできるようにする
    self.assertEqual("1", self.lines[99])
AssertionError: '1' != 'Buzz'
- 1
+ Buzz


----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (failures=1)

テストおじさん: IndexError: list index out of range は解決しましたね。

テストおじさん: AssertionError: '1' != 'Buzz' はリストの最後を評価しているのでこうすればリストの最初の評価になります。

    def test_1から100まで数をプリントできるようにする(self):
        self.assertEqual("1", self.lines[0]) (1)
        self.assertEqual("Buzz", self.lines[99]) (2)

テストおじさん: これでどうですかね・・・オッケー!

$ python -m unittest main_test.MainTest.test_1から100まで数をプリントできるようにする
.
----------------------------------------------------------------------
Ran 1 test in 0.001s

OK

テストおじさん: 残りのテストもリストのインデクスが原因のようなので修正しましょう。

class MainTest(unittest.TestCase):
    def setUp(self):
        with captured_stdout() as stdout:
            execute()
            self.lines = stdout.getvalue().splitlines()

    def test_1から100まで数をプリントできるようにする(self):
        self.assertEqual("1", self.lines[0]) (1)
        self.assertEqual("Buzz", self.lines[99]) (2)

    def test_3の倍数のときは数の代わりにFizzをプリントする(self):
        self.assertEqual("Fizz", self.lines[2]) (3)

    def test_5の倍数のときはBuzzとプリントする(self):
        self.assertEqual("Buzz", self.lines[4]) (4)

    def test_3と5両方の倍数の場合にはFizzBuzzとプリントする(self):
        self.assertEqual("FizzBuzz", self.lines[14]) (5)

テストおじさん: 確認、確認。

$ python main_test.py -v
test_1から100まで数をプリントできるようにする (__main__.MainTest) ... ok
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (__main__.MainTest) ... ok
test_3の倍数のときは数の代わりにFizzをプリントする (__main__.MainTest) ... ok
test_5の倍数のときはBuzzとプリントする (__main__.MainTest) ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.001s

OK
  • ❏ 構造的に機能を付け加えにくいプログラムに、新規機能を追加しなければならない場合には、まず機能追加が簡単になるようにリファクタリングをしてから追加を行うこと。

  • ✓ リファクタリングに入る前に、しっかりとした一連のテスト群が用意できているかを確認すること。これらのテストには自己診断機能が不可欠である。

  • リファクタリングでは小さなステップでプログラムを変更していく。そのため、誤ったことしても、バグを見つけるのは簡単である。

  • コンパイラが理解出るコードは誰にでも書ける。すぐれたプログラマは、人間にとってわかりやすいコードを書く。

  • ❏ リファクタリング(名詞):外側から見たときの振る舞いを保ちつつ、理解や修正が簡単になるように、ソフトウェアの内部構造を変化させること。

  • ❏ リファクタリングする(動詞):一連のリファクタリングを適用して、外部から見た振る舞いの変更なしに、ソフトウェアを再構築すること。

  • ✓ 3三度目になったらリファクタリング開始。

  • ❏ あまり早期にインタフェースを公開しないこと。スムーズなリファクタリングのために、時にはコードの所有権のポリシーを変えることも必要。

  • ❏ コメントの必要を感じたときにはリファクタリングを行って、コメントを書かなくとも内容がわかるようなコードを目指すこと。

  • ❏ テストを完全に自動化して、その結果もテストにチェックさせること。

  • テストをひとそろいにしておくと、バグの検出に絶大な威力を発揮する。これによって、バグの発見にかかる時間は削除される。

テストおじさん: このようにリファクタリングは ベイビーステップ で進めて行くことに留意してください。

ベイビーステップ(Baby Steps)

大きな変更は、大きなステップでやりたくなるものである。距離が長く、時間を書けずにそこまで行くとなれば、そうするしかないように思える。だが、重大な変更を一気に行うのは危険だ。変更を頼まれるのは人間である。変更は不安が伴う。不安を伴えば、人間は変更をすばやくやろうとする。

2.4.3. 学習用テスト

テストおじさん: while文 を使った繰り返し処理では無限ループが発生する恐れがありますよね。

テストおじさん: for文 に書き換えて無限ループが発生しないようにしておきましょう。

テストおじさん: 皆さん同様、私もPythonは詳しくないので 学習用テスト を使って for文 の振る舞いを確認して見ましょう。

学習用テスト

チーム外の誰かが書いたソフトウェアのテストを書くのはどのようなときか----そのソフトウェアの新機能を初めて使う際に書いてみよう。

whileは、条件に設定した値がTrueの間、処理を繰り返し続ける。繰り返す処理部分は右にインデントすること。

forは、コンテナから順に値を取り出して処理を行うための構文。繰り返し実行する部分は、右にインデントして書くこと。

テストおじさん: この場合はソフトウェアのテストと言うより言語仕様の確認テストといった感じですかね。

テストおじさん: 1から5まで for文 で標準出力を リスト に出力するにはこんな感じかな。

データとリスト

こんなとき、「多数の値をまとめて扱えるもの」があれば、ずいぶんとデータ処理もはかどります。 Pythonには、こうした役割を果たすためのデータをまとめるデータ形式がいくつか用意されています。 これらは「コンテナ」と呼ばれます。

リストは、[]か、list()で作成する。作ったリストは、[]にインデクス番号を指定して操作できる。

def execute_for():
    for n in [1, 2, 3, 4, 5]: (1)
        print(n)


class MainTest(unittest.TestCase):
    def test_execute_for(self):
        with captured_stdout() as stdout:
            execute_for()
            lines = stdout.getvalue().splitlines()
        self.assertTrue(1, lines[0])
        self.assertTrue(4, lines[-1])

テスト鬼さん: self.assertTrue(4, lines[-1]) は?

テストおじさん: これは リスト の最後値を指定する場合に使うインデクスです。

テストおじさん: (学習用)テスト。

$ python -m unittest main_test.MainTest.test_execute_for
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

テストおじさん: リスト だとこうすると失敗するのよね。

def execute_for():
    list = [1, 2, 3, 4, 5] (1)
    list[4] = 6
    for n in list:
        print(n)
$ python -m unittest main_test.MainTest.test_execute_for
F
======================================================================
FAIL: test_execute_for (main_test.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/k2works/Projects/hiroshima-arc/re_zero_tdd/dev/20181116/replay/main_test.py", line 32, in test_execute_for
    self.assertEqual("5", lines[-1])
AssertionError: '5' != '6'
- 5
+ 6


----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (failures=1)

テストおじさん: こういう時は タプル を使います。

タプルは、複数の値をひとまとめにして扱うためのもの。()の中に値をカンマで区切って記述する。

def execute_for():
    tuple = (1, 2, 3, 4, 5) (2)
    tuple[4] = 6
    for n in tuple:
        print(n)
$ python -m unittest main_test.MainTest.test_execute_for
E
======================================================================
ERROR: test_execute_for (main_test.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/k2works/Projects/hiroshima-arc/re_zero_tdd/dev/20181116/replay/main_test.py", line 28, in test_execute_for
    execute_for()
  File "/Users/k2works/Projects/hiroshima-arc/re_zero_tdd/dev/20181116/replay/main_test.py", line 21, in execute_for
    tuple[4] = 6
TypeError: 'tuple' object does not support item assignment

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (errors=1)

テストおじさん: TypeError: 'tuple' object does not support item assignment と怒られますね。

テストおじさん: リスト はミュータブルなのに対して タプル はイミュータブルなコンテナとなっています。

ミュータブルは、変更可能。リストがこれに当たる。イミュータブルは、変更不可。タプルがこれに当たる。

コードを修正するテストおじさん。

テストおじさん: これで、オッケー。

def execute_for():
    tuple = (1, 2, 3, 4, 5) (2)
    for n in tuple:
        print(n)
$ python -m unittest main_test.MainTest.test_execute_for
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

テストおじさん: あとは100まで入力すれば・・・ってそれは現実的ではありませんよね。

テストおじさん: そんな時は レンジ を使います。

レンジは、指定した範囲の数列を扱うもの。range()で作成する。 保管されている値は、[]でインデクスで取り出せる。

def execute_for():
    for n in range(100): (3)
        print(n)

class MainTest(unittest.TestCase):
    def test_execute_for(self):
        with captured_stdout() as stdout:
            execute_for()
            lines = stdout.getvalue().splitlines()
        self.assertEqual("1", lines[0])
        self.assertEqual("100", lines[-1])

テストおじさん: (学習用)テスト。

$ python -m unittest main_test.MainTest.test_execute_for
F
======================================================================
FAIL: test_execute_for (main_test.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/k2works/Projects/hiroshima-arc/re_zero_tdd/dev/20181116/replay/main_test.py", line 28, in test_execute_for
    self.assertEqual("1", lines[0])
AssertionError: '1' != '0'
- 1
+ 0


----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (failures=1)

テストおじさん: これでどうよ?

def execute_for():
    for n in range(100): (3)
        print(n)

class MainTest(unittest.TestCase):
    def test_execute_for(self):
        with captured_stdout() as stdout:
            execute_for()
            lines = stdout.getvalue().splitlines()
        self.assertEqual("1", lines[1]) (4)
        self.assertEqual("100", lines[-1])

テストおじさん: (学習用)テスト。

$ python -m unittest main_test.MainTest.test_execute_for
F
======================================================================
FAIL: test_execute_for (main_test.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/k2works/Projects/hiroshima-arc/re_zero_tdd/dev/20181116/replay/main_test.py", line 29, in test_execute_for
    self.assertEqual("100", lines[-1])
AssertionError: '100' != '99'
- 100
+ 99


----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (failures=1)

テストおじさん: えーと、今度は AssertionError: '100' != '99' ・・・ レンジ は指定した値は範囲に含まないんですね。

テストおじさん: ということは、こうして。

def execute_for():
    for n in range(101): (5)
        print(n)


class MainTest(unittest.TestCase):
    def test_execute_for(self):
        with captured_stdout() as stdout:
            execute_for()
            lines = stdout.getvalue().splitlines()
        self.assertEqual("1", lines[1])
        self.assertEqual("100", lines[-1])

テストおじさん: こう。

$ python -m unittest main_test.MainTest.test_execute_for
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

テストおじさん: レンジ を使うことで1から100までの出力を簡単にすることができるようになりました。

テストおじさん: せっかくなので レンジ学習用テスト を別途作成してみましょう。

class RangeTest(unittest.TestCase):
    def test_範囲を示すレンジ(self):
        rg = range(10)
        self.assertEqual(0, rg[0])
        self.assertEqual(9, rg[-1])
        rg = range(5, 10)
        self.assertEqual(5, rg[0])
        self.assertEqual(9, rg[-1])
        rg = range(10, 20, 2)
        self.assertEqual(12, rg[1])
        self.assertEqual(18, rg[-1])

    def test_レンジのシーケンス演算(self):
        rg = range(30)
        self.assertTrue(10 in rg)
        self.assertTrue(30 not in rg)
        rg = range(50)
        self.assertEqual(range(1, 4), rg[1:4])
        self.assertEqual(50, len(rg))
        self.assertEqual(49, max(rg))
        self.assertEqual(0, min(rg))

    def test_シーケンス間の変換(self):
        list1 = [100, 200, 300]
        tpl1 = (123, 'ok', True)
        rng1 = range(10, 20)
        result = list1 + list(tpl1) + list(rng1)
        self.assertEqual([100, 200, 300, 123, 'ok', True, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19], result)

テストおじさん: レンジの学習用テストだけ実行する場合はこうですね。

$ python -m unittest main_test.RangeTest -v
test_シーケンス間の変換 (main_test.RangeTest) ... ok
test_レンジのシーケンス演算 (main_test.RangeTest) ... ok
test_範囲を示すレンジ (main_test.RangeTest) ... ok

----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK

2.4.4. アルゴリズムの取り替え

テストおじさん: 学習用テストfor文レンジ の使い方がわかったので アルゴリズムの取り替え を実施しましょう。

アルゴリズムの取り替え

アルゴリズムをよりわかりやすいものに置き換えたい。

メソッドの本体を新たなアルゴリズムで置き換える。

テストおじさん: まずは 学習用テスト 関数を使って 明白な実装 をします。

def execute_for():
    for n in range(101):
        if n % 3 == 0 and n % 5 == 0:
            print("FizzBuzz")
        elif n % 3 == 0:
            print("Fizz")
        elif n % 5 == 0:
            print("Buzz")
        else:
            print(n)
        n = n + 1



class MainTest(unittest.TestCase):
    def test_execute_for(self):
        with captured_stdout() as stdout:
            execute_for()
            lines = stdout.getvalue().splitlines()
        self.assertEqual("1", lines[1])
        self.assertEqual("100", lines[-1])

テストおじさん: どうかな?

$ python -m unittest main_test.MainTest.test_execute_for
F
======================================================================
FAIL: test_execute_for (main_test.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/k2works/Projects/hiroshima-arc/re_zero_tdd/dev/20181116/replay/main_test.py", line 39, in test_execute_for
    self.assertEqual("100", lines[-1])
AssertionError: '100' != 'Buzz'
- 100
+ Buzz


----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (failures=1)

テストおじさん: AssertionError: '100' != 'Buzz' ・・・あー100は5で割り切れるからこれはテストが間違っているね。

テストおじさん: いや、設計の仕様が間違っているかな。

    def test_execute_for(self):
        with captured_stdout() as stdout:
            execute_for()
            lines = stdout.getvalue().splitlines()
        self.assertEqual("1", lines[1])
        self.assertEqual("Buzz", lines[-1]) (1)

テストおじさん: これで・・・オッケー。

$ python -m unittest main_test.MainTest.test_execute_for
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

テストおじさん: n = n + 1 の変数への代入は不要なので削除して。

def execute_for():
    for n in range(101):
        if n % 3 == 0 and n % 5 == 0:
            print("FizzBuzz")
        elif n % 3 == 0:
            print("Fizz")
        elif n % 5 == 0:
            print("Buzz")
        else:
            print(n)

テストおじさん: プログラムは壊れてないよね。

$ python main_test.py -v
test_1から100まで数をプリントできるようにする (__main__.MainTest) ... ok
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (__main__.MainTest) ... ok
test_3の倍数のときは数の代わりにFizzをプリントする (__main__.MainTest) ... ok
test_5の倍数のときはBuzzとプリントする (__main__.MainTest) ... ok
test_execute_for (__main__.MainTest) ... ok
test_シーケンス間の変換 (__main__.RangeTest) ... ok
test_レンジのシーケンス演算 (__main__.RangeTest) ... ok
test_範囲を示すレンジ (__main__.RangeTest) ... ok

----------------------------------------------------------------------
Ran 8 tests in 0.001s

OK

テストおじさん: よし、それでは execute_for 関数を execute に差し替えましょう。

def execute_for(): (2)
    n = 1
    while n != 101:
        if n % 3 == 0 and n % 5 == 0:
            print("FizzBuzz")
        elif n % 3 == 0:
            print("Fizz")
        elif n % 5 == 0:
            print("Buzz")
        else:
            print(n)
        n = n + 1


def execute(): (3)
    for n in range(101):
        if n % 3 == 0 and n % 5 == 0:
            print("FizzBuzz")
        elif n % 3 == 0:
            print("Fizz")
        elif n % 5 == 0:
            print("Buzz")
        else:
            print(n)

テストおじさん: 確認・・・ファッ!

$ python -m unittest main_test.MainTest -v
test_1から100まで数をプリントできるようにする (main_test.MainTest) ... FAIL
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (main_test.MainTest) ... FAIL
test_3の倍数のときは数の代わりにFizzをプリントする (main_test.MainTest) ... FAIL
test_5の倍数のときはBuzzとプリントする (main_test.MainTest) ... FAIL
test_execute_for (main_test.MainTest) ... FAIL

======================================================================
FAIL: test_1から100まで数をプリントできるようにする (main_test.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/k2works/Projects/hiroshima-arc/re_zero_tdd/dev/20181116/replay/main_test.py", line 46, in test_1から100まで数をプリントできるようにする
    self.assertEqual("1", self.lines[0])
AssertionError: '1' != 'FizzBuzz'
- 1
+ FizzBuzz


======================================================================
FAIL: test_3と5両方の倍数の場合にはFizzBuzzとプリントする (main_test.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/k2works/Projects/hiroshima-arc/re_zero_tdd/dev/20181116/replay/main_test.py", line 56, in test_3と5両方の倍数の場合にはFizzBuzzとプリントする
    self.assertEqual("FizzBuzz", self.lines[14])
AssertionError: 'FizzBuzz' != '14'
- FizzBuzz
+ 14


======================================================================
FAIL: test_3の倍数のときは数の代わりにFizzをプリントする (main_test.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/k2works/Projects/hiroshima-arc/re_zero_tdd/dev/20181116/replay/main_test.py", line 50, in test_3の倍数のときは数の代わりにFizzをプリントする
    self.assertEqual("Fizz", self.lines[2])
AssertionError: 'Fizz' != '2'
- Fizz
+ 2


======================================================================
FAIL: test_5の倍数のときはBuzzとプリントする (main_test.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/k2works/Projects/hiroshima-arc/re_zero_tdd/dev/20181116/replay/main_test.py", line 53, in test_5の倍数のときはBuzzとプリントする
    self.assertEqual("Buzz", self.lines[4])
AssertionError: 'Buzz' != '4'
- Buzz
+ 4


======================================================================
FAIL: test_execute_for (main_test.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/k2works/Projects/hiroshima-arc/re_zero_tdd/dev/20181116/replay/main_test.py", line 37, in test_execute_for
    self.assertEqual("1", lines[1])
AssertionError: '1' != '2'
- 1
+ 2


----------------------------------------------------------------------
Ran 5 tests in 0.001s

FAILED (failures=5)

テストおじさん: またまた、テストが壊れてしまいました。

テストおじさん: ベイビーステップ ベイビーステップ ・・・

テスト結果を眺めるテストおじさん

======================================================================
FAIL: test_1から100まで数をプリントできるようにする (main_test.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/k2works/Projects/hiroshima-arc/re_zero_tdd/dev/20181116/replay/main_test.py", line 46, in test_1から100まで数をプリントできるようにする
    self.assertEqual("1", self.lines[0])
AssertionError: '1' != 'FizzBuzz'
- 1
+ FizzBuzz

======================================================================
FAIL: test_execute_for (main_test.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/k2works/Projects/hiroshima-arc/re_zero_tdd/dev/20181116/replay/main_test.py", line 37, in test_execute_for
    self.assertEqual("1", lines[1])
AssertionError: '1' != '2'
- 1
+ 2

テストおじさん: execute 関数が AssertionError: '1' != 'FizzBuzz'execute_forAssertionError: '1' != '2' となってるということは・・・

テストおじさん: また リスト のインデクスが問題のようですね。

テストおじさん: execute_for 関数はこうかな・・・よし!

    def test_execute_for(self):
        with captured_stdout() as stdout:
            execute_for()
            lines = stdout.getvalue().splitlines()
        self.assertEqual("1", lines[0]) (3)
        self.assertEqual("Buzz", lines[-1])
$ python -m unittest main_test.MainTest.test_execute_for
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

テストおじさん: となると execute 関数はこうね・・・よし!

    def test_1から100まで数をプリントできるようにする(self):
        self.assertEqual("1", self.lines[1]) (4)
        self.assertEqual("Buzz", self.lines[100]) (5)
$ python -m unittest main_test.MainTest.test_1から100まで数をプリントできるようにする
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

テストおじさん: 残りも同じっすね・・・オッケー!

    def test_3の倍数のときは数の代わりにFizzをプリントする(self):
        self.assertEqual("Fizz", self.lines[3]) (6)

    def test_5の倍数のときはBuzzとプリントする(self):
        self.assertEqual("Buzz", self.lines[5]) (7)

    def test_3と5両方の倍数の場合にはFizzBuzzとプリントする(self):
        self.assertEqual("FizzBuzz", self.lines[15]) (8)
$ python -m unittest main_test.MainTest -v
test_1から100まで数をプリントできるようにする (main_test.MainTest) ... ok
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (main_test.MainTest) ... ok
test_3の倍数のときは数の代わりにFizzをプリントする (main_test.MainTest) ... ok
test_5の倍数のときはBuzzとプリントする (main_test.MainTest) ... ok
test_execute_for (main_test.MainTest) ... ok

----------------------------------------------------------------------
Ran 5 tests in 0.001s

OK

テストおじさん: 後は不要な execute_for 関数を削除して アルゴリズムの取り替え 完了です。

テストおじさん: プログラムが壊れてないか確認・・・おう!?

$ python -m unittest main_test.MainTest -v
test_1から100まで数をプリントできるようにする (main_test.MainTest) ... ok
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (main_test.MainTest) ... ok
test_3の倍数のときは数の代わりにFizzをプリントする (main_test.MainTest) ... ok
test_5の倍数のときはBuzzとプリントする (main_test.MainTest) ... ok
test_execute_for (main_test.MainTest) ... ERROR

======================================================================
ERROR: test_execute_for (main_test.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/k2works/Projects/hiroshima-arc/re_zero_tdd/dev/20181116/replay/main_test.py", line 20, in test_execute_for
    execute_for()
NameError: name 'execute_for' is not defined

----------------------------------------------------------------------
Ran 5 tests in 0.001s

FAILED (errors=1)

テストおじさん: NameError: name 'execute_for' is not defined ・・・あー関数を削除したからね。

    def test_execute_for(self): (9)
        with captured_stdout() as stdout:
            execute_for()
            lines = stdout.getvalue().splitlines()
        self.assertEqual("1", lines[0])
        self.assertEqual("Buzz", lines[-1])

テストおじさん: test_execute_for も不要なので削除して確認・・・オッケー!

$ python -m unittest main_test.MainTest -v
test_1から100まで数をプリントできるようにする (main_test.MainTest) ... ok
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (main_test.MainTest) ... ok
test_3の倍数のときは数の代わりにFizzをプリントする (main_test.MainTest) ... ok
test_5の倍数のときはBuzzとプリントする (main_test.MainTest) ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.001s

OK

2.4.5. 長すぎるコード

テストおじさん: さて execute 関数が実行している内容ですが・・・

コメント追加するテストおじさん。

def execute():
    # 100回繰り返す (1)
    for n in range(101):
        # 3で割り切れたらFizz 5で割り切れたらBuzz 3または5で割り切れたらFizzBuzzをプリントする (2)
        if n % 3 == 0 and n % 5 == 0:
            print("FizzBuzz")
        elif n % 3 == 0:
            print("Fizz")
        elif n % 5 == 0:
            print("Buzz")
        else:
            print(n)

テストおじさん: コメントが必要になる 長すぎるコード ですね。

  • ❏ 構造的に機能を付け加えにくいプログラムに、新規機能を追加しなければならない場合には、まず機能追加が簡単になるようにリファクタリングをしてから追加を行うこと。

  • ✓ リファクタリングに入る前に、しっかりとした一連のテスト群が用意できているかを確認すること。これらのテストには自己診断機能が不可欠である。

  • ✓ リファクタリングでは小さなステップでプログラムを変更していく。そのため、誤ったことしても、バグを見つけるのは簡単である。

  • ✓ コンパイラが理解出るコードは誰にでも書ける。すぐれたプログラマは、人間にとってわかりやすいコードを書く。

  • ❏ リファクタリング(名詞):外側から見たときの振る舞いを保ちつつ、理解や修正が簡単になるように、ソフトウェアの内部構造を変化させること。

  • ❏ リファクタリングする(動詞):一連のリファクタリングを適用して、外部から見た振る舞いの変更なしに、ソフトウェアを再構築すること。

  • ✓ 3三度目になったらリファクタリング開始。

  • ❏ あまり早期にインタフェースを公開しないこと。スムーズなリファクタリングのために、時にはコードの所有権のポリシーを変えることも必要。

  • コメントの必要を感じたときにはリファクタリングを行って、コメントを書かなくとも内容がわかるようなコードを目指すこと。

  • ❏ テストを完全に自動化して、その結果もテストにチェックさせること。

  • ✓ テストをひとそろいにしておくと、バグの検出に絶大な威力を発揮する。これによって、バグの発見にかかる時間は削除される。

テストおじさん: どうやらリファクタリングの必要がありそうです。

テストおじさん: まず、繰り返し内部の処理に対して メソッドの抽出 を実施します。

def execute():
    # 100回繰り返す
    for n in range(101):
        # 3で割り切れたらFizz 5で割り切れたらBuzz 3または5で割り切れたらFizzBuzzをプリントする
        fizz_buzz(n) (3)


def fizz_buzz(n):
    if n % 3 == 0 and n % 5 == 0:
        print("FizzBuzz")
    elif n % 3 == 0:
        print("Fizz")
    elif n % 5 == 0:
        print("Buzz")
    else:
        print(n)

テストおじさん: 変更したら、確認です。

$ python -m unittest main_test.MainTest -v
test_1から100まで数をプリントできるようにする (main_test.MainTest) ... ok
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (main_test.MainTest) ... ok
test_3の倍数のときは数の代わりにFizzをプリントする (main_test.MainTest) ... ok
test_5の倍数のときはBuzzとプリントする (main_test.MainTest) ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.001s

OK

テストおじさん: execute 関数は繰り返し処理と fizz_buzz 関数の実行しています。

テストおじさん: ここは 1つのことを行う に従って繰り返し部分も メソッドの抽出 を実施しましょう。

1つのことを行う

関数では1つのことを行うようにせよ。その1つのことをきちんと行い、それ以外のことを行ってはならない。

def execute():
    # 100回繰り返す
    iterate(101) (4)


def iterate(c):
    for n in range(c):
        # 3で割り切れたらFizz 5で割り切れたらBuzz 3または5で割り切れたらFizzBuzzをプリントする
        fizz_buzz(n)


def fizz_buzz(n):
    if n % 3 == 0 and n % 5 == 0:
        print("FizzBuzz")
    elif n % 3 == 0:
        print("Fizz")
    elif n % 5 == 0:
        print("Buzz")
    else:
        print(n)

テストおじさん: 大丈夫・・・ですね。

$ python -m unittest main_test.MainTest -v
test_1から100まで数をプリントできるようにする (main_test.MainTest) ... ok
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (main_test.MainTest) ... ok
test_3の倍数のときは数の代わりにFizzをプリントする (main_test.MainTest) ... ok
test_5の倍数のときはBuzzとプリントする (main_test.MainTest) ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.001s

OK

2.4.6. 説明用変数の導入

テストおじさん: さて メソッドの抽出 により見通しの良いコードになりましたが・・・・

コードを眺めるテストおじさん。

def execute():
    # 100回繰り返す
    iterate(101)


def iterate(c): (1)
    for n in range(c):
        # 3で割り切れたらFizz 5で割り切れたらBuzz 3または5で割り切れたらFizzBuzzをプリントする
        fizz_buzz(n)


def fizz_buzz(n): (2)
    if n % 3 == 0 and n % 5 == 0:
        print("FizzBuzz")
    elif n % 3 == 0:
        print("Fizz")
    elif n % 5 == 0:
        print("Buzz")
    else:
        print(n)

テストおじさん: 関数の間で nc といった変数を使っていますがぱっと見なにか分かりづらいですね。

テストおじさん: 説明変数の導入 を導入して読みやすいコードにしましょう。

説明用変数の導入

複雑な式がある。

その式の結果または部分的な結果を、その目的を説明する名前をつけた一時変数に代入する。

テストおじさん: countnumber という変数名に変更しましょう。

def execute():
    # 100回繰り返す
    count = 101
    iterate(count) (3)


def iterate(count): (3)
    for number in range(count):
        # 3で割り切れたらFizz 5で割り切れたらBuzz 3または5で割り切れたらFizzBuzzをプリントする
        fizz_buzz(number)


def fizz_buzz(number): (4)
    if number % 3 == 0 and number % 5 == 0:
        print("FizzBuzz")
    elif number % 3 == 0:
        print("Fizz")
    elif number % 5 == 0:
        print("Buzz")
    else:
        print(number)

テストおじさん: テスト・・・オッケー。

$ python -m unittest main_test.MainTest -v
test_1から100まで数をプリントできるようにする (main_test.MainTest) ... ok
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (main_test.MainTest) ... ok
test_3の倍数のときは数の代わりにFizzをプリントする (main_test.MainTest) ... ok
test_5の倍数のときはBuzzとプリントする (main_test.MainTest) ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.001s

OK

2.4.7. 関数と変数と定数

テストおじさん: fizz_buzz 関数 を見てください。

関数には、名前・引数・戻り値といった要素がある。関数は名前と引数を使って定義する。

def fizz_buzz(number):
    if number % 3 == 0 and number % 5 == 0:
        print("FizzBuzz") (1)
    elif number % 3 == 0:
        print("Fizz") (2)
    elif number % 5 == 0:
        print("Buzz") (3)
    else:
        print(number) (4)

テストおじさん: 一時 変数 を使って 重複したコード である print 関数 の呼び出しを一回にしてみましょう。

変数は、値を保管するための入れ物。=演算子を使い、値を代入すると自動的に作成される。

def fizz_buzz(number):
    value = number (5)

    if number % 3 == 0 and number % 5 == 0:
        value = "FizzBuzz"  (5)
    elif number % 3 == 0:
        value = "Fizz" (5)
    elif number % 5 == 0:
        value = "Buzz" (5)

    print(value) (6)
$ python -m unittest main_test.MainTest -v
test_1から100まで数をプリントできるようにする (main_test.MainTest) ... ok
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (main_test.MainTest) ... ok
test_3の倍数のときは数の代わりにFizzをプリントする (main_test.MainTest) ... ok
test_5の倍数のときはBuzzとプリントする (main_test.MainTest) ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.001s

OK

テストおじさん: 文字 リテラル定数 に抽出して変更箇所をわかりやすくしましょう。

リテラルについて

Pythonの値は、さまざまな形で利用されます。もっとも多いのは、スクリプトの中に直接、値を記述する方法です。 こうしたスクリプトに直接記述される値のことをリテラルと呼びます。

すべて大文字は定数扱い

これは基本的なマナーであって守らなければならないことではありませんが、Pythonでは、定数(後で値を変更したりしない特所な変数)として扱う変数は、すべて大文字の名前をつけるのが一般的です。 例えば変数ABCは定数です。

BUZZ = "Buzz" (7)
FIZZ = "Fizz" (7)
FIZZ_BUZZ = "FizzBuzz" (7)

.....

def fizz_buzz(number):
    value = number

    if number % 3 == 0 and number % 5 == 0:
        value = FIZZ_BUZZ (7)
    elif number % 3 == 0:
        value = FIZZ (7)
    elif number % 5 == 0:
        value = BUZZ (7)

    print(value)
$ python -m unittest main_test.MainTest -v
test_1から100まで数をプリントできるようにする (main_test.MainTest) ... ok
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (main_test.MainTest) ... ok
test_3の倍数のときは数の代わりにFizzをプリントする (main_test.MainTest) ... ok
test_5の倍数のときはBuzzとプリントする (main_test.MainTest) ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.001s

OK

2.4.8. 副作用のある関数・ない関数

execute 関数 を見ながら

def execute():
    # 100回繰り返す
    count = 101
    iterate(count)

テスト初めて書いたひと: この関数からは繰り返しを実行していること以外は読み取れませんね。

テストおじさん: そうですね、詳細は iterate 関数 を読まないとわかりませんね。

テストおじさん: execute 関数 で処理の概要が把握できるようにしましょう。

テストおじさん: まず、fizz_buzz 関数 を値を返す 関数 に変更します。

def iterate(count):
    for number in range(count):
        # 3で割り切れたらFizz 5で割り切れたらBuzz 3または5で割り切れたらFizzBuzzをプリントする
        print(fizz_buzz(number)) (2)


def fizz_buzz(number):
    value = number

    if number % 3 == 0 and number % 5 == 0:
        value = FIZZ_BUZZ
    elif number % 3 == 0:
        value = FIZZ
    elif number % 5 == 0:
        value = BUZZ

    return value (1)

テストおじさん: テストが壊れないように iterate 関数print 関数 を実行するようにします。

$ python -m unittest main_test.MainTest -v
test_1から100まで数をプリントできるようにする (main_test.MainTest) ... ok
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (main_test.MainTest) ... ok
test_3の倍数のときは数の代わりにFizzをプリントする (main_test.MainTest) ... ok
test_5の倍数のときはBuzzとプリントする (main_test.MainTest) ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.001s

OK

テストおじさん: 次に、関数 間で値を共有できるように グローバル 変数リスト を作ります。

values = [] (3)


def execute():
    # 100回繰り返す
    count = 101
    iterate(count)
    for value in values: (5)
        print(value)


def iterate(count):
    for number in range(count):
        # 3で割り切れたらFizz 5で割り切れたらBuzz 3または5で割り切れたらFizzBuzzをプリントする
        values.append(fizz_buzz(number)) (4)

テストおじさん: テストが壊れないように execute 関数print 関数 を実行するようにします。

$ python -m unittest main_test.MainTest -v
test_1から100まで数をプリントできるようにする (main_test.MainTest) ... ok
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (main_test.MainTest) ... ok
test_3の倍数のときは数の代わりにFizzをプリントする (main_test.MainTest) ... ok
test_5の倍数のときはBuzzとプリントする (main_test.MainTest) ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.001s

OK

リモートさん: fizz_buzz 関数 から副作用の print が消えたっ!

テストおじさん: 副作用の無い fizz_buzz 関数 の結果を 副作用のある iterate 関数 が グローバル 変数 に値を設定した結果 execute 関数 で処理の概要を読み取れるようになりました。

def execute():
    # 100回繰り返す
    count = 101
    iterate(count)
    for value in values: (5)
        print(value)

2.4.9. コメント

テストおじさん: execute 関数 の見通しは良くなりましたが・・・

def execute():
    # 100回繰り返す (2)
    count = 101
    iterate(count)
    for value in values:
        print(value)


def iterate(count):
    for number in range(count):
        # 3で割り切れたらFizz 5で割り切れたらBuzz 3または5で割り切れたらFizzBuzzをプリントする (1)
        values.append(fizz_buzz(number))

テストおじさん: コメント の臭いがしますね。

コメント

コメントを書いてはいけないなどと言うつもりはまったくありません。 コメントは決して悪い臭いではなく、むしろいい香りなのです。 ここでコメントについて言及しているのは、コメントが消臭剤として使われることがあるからです。 コメントが非常に丁寧に書かれていたのは、実はわかりにくいコードを補うためだったということがよくあるのです。

テストおじさん: まず、iterate 関数ですが実際にやっていることとコメントの内容が違いますね。

def iterate(count):
    for number in range(count):
        # 3で割り切れたらFizz 5で割り切れたらBuzz 3または5で割り切れたらFizzBuzzをプリントする (1)
        values.append(fizz_buzz(number))

テストおじさん: こーゆーのをコメントバグというのですがここは 説明用変数の導入 を使って 自分自身をコードの中で説明する ようにしましょう。

自分自身をコード中で説明する

あなたの意図をコードに込めるのに必要なのは、たったの数秒の思考です。 大抵の場合、それは単にコメントに書きたかった内容を実行する関数を作成するだけなのです。

def iterate(count):
    for number in range(count):
        value = fizz_buzz(number)
        values.append(value)
$ python -m unittest main_test.MainTest -v
test_1から100まで数をプリントできるようにする (main_test.MainTest) ... ok
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (main_test.MainTest) ... ok
test_3の倍数のときは数の代わりにFizzをプリントする (main_test.MainTest) ... ok
test_5の倍数のときはBuzzとプリントする (main_test.MainTest) ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.001s

OK

テストおじさん: 次に execute 関数ですが・・・

def execute():
    # 100回繰り返す (2)
    count = 101
    iterate(count)
    for value in values:
        print(value)

テストおじさん: 100回繰り返すのに count 変数には101を代入している。これは レンジ の仕様なのですがちょっと違和感ありますよね。

テストおじさん: グローバル 変数 を使って count を共有すると共に有益なコメントに書き換えて 意図の説明 をしましょう。

意図の説明

時折、コメントは単に実装に対する有益な情報にとどまらず、決定の裏に隠された意図を伝える場合があります。

count = 100


def execute():
    global count
    # レンジは指定された値を範囲に含めないので1プラスする (3)
    count = count + 1
    iterate()
    for value in values:
        print(value)


def iterate():
    for number in range(count):
        value = fizz_buzz(number)
        values.append(value)
$ python -m unittest main_test.MainTest -v
test_1から100まで数をプリントできるようにする (main_test.MainTest) ... ok
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (main_test.MainTest) ... ok
test_3の倍数のときは数の代わりにFizzをプリントする (main_test.MainTest) ... ok
test_5の倍数のときはBuzzとプリントする (main_test.MainTest) ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.001s

OK

2.4.10. 抽象レベルが正しくないコード

テストおじさん: 次に execute 関数 ですが・・・

def execute():
    global count
    # レンジは指定された値を範囲に含めないので1プラスする
    count = count + 1 (1)
    iterate() (2)
    for value in values: (3)
        print(value)

テストおじさん: 変数 への代入、 関数 の呼び出し、 の実行と 抽象レベルが正しくないコード になっています。

スクリプトは、実行する命令などを1つずつ順番に書いていきます。 この実行する処理を書いたもののひとかたまりを文と呼びます。

G6: 抽象レベルが正しくないコード

抽象化を行うことは重要なことです。抽象化とは、高いレベルの概念と低いレベルの詳細な概念とを分離することです。

テストおじさん: メソッドの抽出 を実施して処理の抽出レベルをあわせましょう。

テストおじさん: 副作用のあるカウントの設定は 名前で副作用を示すべき に従って。

N7: 名前で副作用を示すべき

関数、変数、クラスが何であり、何をするかを名前で表現すべきです。副作用を名前の下に隠してはいけません。 関数に1つの動詞を名前として付けたら、その関数の名前に書かれたこと以上のことを処理させるのは避けてください。

def execute():
    set_count() (4)
    iterate()
    for value in values:
        print(value)


def set_count():
    global count
    # レンジは指定された値を範囲に含めないので1プラスする
    count = count + 1
$ python -m unittest main_test.MainTest -v
test_1から100まで数をプリントできるようにする (main_test.MainTest) ... ok
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (main_test.MainTest) ... ok
test_3の倍数のときは数の代わりにFizzをプリントする (main_test.MainTest) ... ok
test_5の倍数のときはBuzzとプリントする (main_test.MainTest) ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.001s

OK

テストおじさん: 同様に副作用のある値のプリントはこう。

def execute():
    set_count()
    iterate()
    print_values() (5)


def print_values():
    for value in values:
        print(value)
$ python -m unittest main_test.MainTest -v
test_1から100まで数をプリントできるようにする (main_test.MainTest) ... ok
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (main_test.MainTest) ... ok
test_3の倍数のときは数の代わりにFizzをプリントする (main_test.MainTest) ... ok
test_5の倍数のときはBuzzとプリントする (main_test.MainTest) ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.001s

OK

テストおじさん: 処理の抽出レベルは一致したのですが iterate 関数は 関数名は体を表すべき を満たしているとは言えませんね。

G20: 関数名は体を表すべき

関数が何を行うかを知るために、実装(あるいはドキュメント)を見なければならないようなら、もっとよい名前を探すか、提供する機能を整理して、もっとよい名前を持った複数の関数に分けるべきです。

テストおじさん: ここはこうして。

def execute():
    set_count()
    set_values_by_fizz_buzz() (6)
    print_values()


def set_count():
    global count
    # レンジは指定された値を範囲に含めないので1プラスする
    count = count + 1


def set_values_by_fizz_buzz():
    for number in range(count):
        value = fizz_buzz(number)
        values.append(value)


def print_values():
    for value in values:
        print(value)
$ python -m unittest main_test.MainTest -v
test_1から100まで数をプリントできるようにする (main_test.MainTest) ... ok
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (main_test.MainTest) ... ok
test_3の倍数のときは数の代わりにFizzをプリントする (main_test.MainTest) ... ok
test_5の倍数のときはBuzzとプリントする (main_test.MainTest) ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.001s

OK

テストおじさん: これで 関数は1つの抽象レベルを担うべき という条件をみたすことができました。

G34: 関数は1つの抽象レベルを担うべき

1つの関数の中の文は同じ抽象レベルで書かれるべきで、それらは関数の名前で表現された操作の1つ下のレベルとなるべきです。

2.4.11. データの抽象化

テストおじさん: グローバル変数 を使って値を共有していますが・・・

おもむろにコードを追加してテストを実行するテストをおじさん。

BUZZ = "Buzz"
FIZZ = "Fizz"
FIZZ_BUZZ = "FizzBuzz"
values = []
count = 100


def execute():
    set_count()
    set_values_by_fizz_buzz()
    print_values()


def set_count():
    global count
    # レンジは指定された値を範囲に含めないので1プラスする
    count = count + 1


def set_values_by_fizz_buzz():
    for number in range(count):
        value = fizz_buzz(number)
        values.append(value)


def print_values():
    for value in values:
        print(value)


def fizz_buzz(number):
    value = number

    if number % 3 == 0 and number % 5 == 0:
        value = FIZZ_BUZZ
    elif number % 3 == 0:
        value = FIZZ
    elif number % 5 == 0:
        value = BUZZ

    return value

del values (1)
$ python -m unittest main_test.MainTest -v
test_1から100まで数をプリントできるようにする (main_test.MainTest) ... ERROR
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (main_test.MainTest) ... ERROR
test_3の倍数のときは数の代わりにFizzをプリントする (main_test.MainTest) ... ERROR
test_5の倍数のときはBuzzとプリントする (main_test.MainTest) ... ERROR

======================================================================
ERROR: test_1から100まで数をプリントできるようにする (main_test.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/k2works/Projects/hiroshima-arc/re_zero_tdd/dev/20181116/replay/main_test.py", line 51, in setUp
    execute()
  File "/Users/k2works/Projects/hiroshima-arc/re_zero_tdd/dev/20181116/replay/main_test.py", line 13, in execute
    set_values_by_fizz_buzz()
  File "/Users/k2works/Projects/hiroshima-arc/re_zero_tdd/dev/20181116/replay/main_test.py", line 26, in set_values_by_fizz_buzz
    values.append(value)
NameError: name 'values' is not defined

======================================================================
ERROR: test_3と5両方の倍数の場合にはFizzBuzzとプリントする (main_test.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/k2works/Projects/hiroshima-arc/re_zero_tdd/dev/20181116/replay/main_test.py", line 51, in setUp
    execute()
  File "/Users/k2works/Projects/hiroshima-arc/re_zero_tdd/dev/20181116/replay/main_test.py", line 13, in execute
    set_values_by_fizz_buzz()
  File "/Users/k2works/Projects/hiroshima-arc/re_zero_tdd/dev/20181116/replay/main_test.py", line 26, in set_values_by_fizz_buzz
    values.append(value)
NameError: name 'values' is not defined

======================================================================
ERROR: test_3の倍数のときは数の代わりにFizzをプリントする (main_test.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/k2works/Projects/hiroshima-arc/re_zero_tdd/dev/20181116/replay/main_test.py", line 51, in setUp
    execute()
  File "/Users/k2works/Projects/hiroshima-arc/re_zero_tdd/dev/20181116/replay/main_test.py", line 13, in execute
    set_values_by_fizz_buzz()
  File "/Users/k2works/Projects/hiroshima-arc/re_zero_tdd/dev/20181116/replay/main_test.py", line 26, in set_values_by_fizz_buzz
    values.append(value)
NameError: name 'values' is not defined

======================================================================
ERROR: test_5の倍数のときはBuzzとプリントする (main_test.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/k2works/Projects/hiroshima-arc/re_zero_tdd/dev/20181116/replay/main_test.py", line 51, in setUp
    execute()
  File "/Users/k2works/Projects/hiroshima-arc/re_zero_tdd/dev/20181116/replay/main_test.py", line 13, in execute
    set_values_by_fizz_buzz()
  File "/Users/k2works/Projects/hiroshima-arc/re_zero_tdd/dev/20181116/replay/main_test.py", line 26, in set_values_by_fizz_buzz
    values.append(value)
NameError: name 'values' is not defined

----------------------------------------------------------------------
Ran 4 tests in 0.001s

FAILED (errors=4)

テストおじさん: こうでも

values.append(10) (2)
$ python -m unittest main_test.MainTest -v
test_1から100まで数をプリントできるようにする (main_test.MainTest) ... FAIL
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (main_test.MainTest) ... FAIL
test_3の倍数のときは数の代わりにFizzをプリントする (main_test.MainTest) ... FAIL
test_5の倍数のときはBuzzとプリントする (main_test.MainTest) ... FAIL

======================================================================
FAIL: test_1から100まで数をプリントできるようにする (main_test.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/k2works/Projects/hiroshima-arc/re_zero_tdd/dev/20181116/replay/main_test.py", line 55, in test_1から100まで数をプリントできるようにする
    self.assertEqual("1", self.lines[1])
AssertionError: '1' != 'FizzBuzz'
- 1
+ FizzBuzz


======================================================================
FAIL: test_3と5両方の倍数の場合にはFizzBuzzとプリントする (main_test.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/k2works/Projects/hiroshima-arc/re_zero_tdd/dev/20181116/replay/main_test.py", line 65, in test_3と5両方の倍数の場合にはFizzBuzzとプリントする
    self.assertEqual("FizzBuzz", self.lines[15])
AssertionError: 'FizzBuzz' != '14'
- FizzBuzz
+ 14


======================================================================
FAIL: test_3の倍数のときは数の代わりにFizzをプリントする (main_test.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/k2works/Projects/hiroshima-arc/re_zero_tdd/dev/20181116/replay/main_test.py", line 59, in test_3の倍数のときは数の代わりにFizzをプリントする
    self.assertEqual("Fizz", self.lines[3])
AssertionError: 'Fizz' != '2'
- Fizz
+ 2


======================================================================
FAIL: test_5の倍数のときはBuzzとプリントする (main_test.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/k2works/Projects/hiroshima-arc/re_zero_tdd/dev/20181116/replay/main_test.py", line 62, in test_5の倍数のときはBuzzとプリントする
    self.assertEqual("Buzz", self.lines[5])
AssertionError: 'Buzz' != '4'
- Buzz
+ 4


----------------------------------------------------------------------
Ran 4 tests in 0.001s

FAILED (failures=4)

テストおじさん: このように グローバル変数 はいつ誰がどこかで書き換えるかわかりません。

関数内で定義した変数は、その関数の中でしか使えない。関数の外側で定義した変数は、関数の中でも使える。

テストおじさん: execute 関数 スコープ範囲だけで共有できればいいのですが現状のコードではそれは難しい・・・

テストおじさん: とりあえず 辞書 を使ってFizzBuzzで使う抽象データであることをコードで表現しておきましょう。

辞書は、キーワードで値を管理する。{}を使うか、dict()で作成できる。 値は、[]にキーワードを指定してやり取りする。

fizz_buzz_date = {
    'values': [],
    'count': 100
} (3)

....

def set_count():
    # レンジは指定された値を範囲に含めないので1プラスする
    fizz_buzz_date['count'] = fizz_buzz_date['count'] + 1 (4)


def set_values_by_fizz_buzz():
    for number in range(fizz_buzz_date['count']): (5)
        value = fizz_buzz(number)
        fizz_buzz_date['values'].append(value) (6)


def print_values():
    for value in fizz_buzz_date['values']: (7)
        print(value)
$ python -m unittest main_test.MainTest -v
test_1から100まで数をプリントできるようにする (main_test.MainTest) ... ok
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (main_test.MainTest) ... ok
test_3の倍数のときは数の代わりにFizzをプリントする (main_test.MainTest) ... ok
test_5の倍数のときはBuzzとプリントする (main_test.MainTest) ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.001s

OK

テストおじさん: 辞書 でも グローバル変数 であることに変わりありませんが読み手にスコープを意識させることはできますね。

2.4.12. キーワード引数とデフォルト値

テストおじさん: そういえば1から10まで出力する場合はどうしましょうかね。

テストをコードを追加するテストおじさん。

    def test_1から10まで数をプリントする(self): (1)
        with captured_stdout() as stdout:
            execute(10)
            self.lines = stdout.getvalue().splitlines()
        self.assertNotIn("11", self.lines)
        self.assertEqual("Buzz", self.lines[10])
$ python -m unittest main_test.MainTest -v
test_1から100まで数をプリントできるようにする (main_test.MainTest) ... ok
test_1から10まで数をプリントする (main_test.MainTest) ... ERROR
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (main_test.MainTest) ... ok
test_3の倍数のときは数の代わりにFizzをプリントする (main_test.MainTest) ... ok
test_5の倍数のときはBuzzとプリントする (main_test.MainTest) ... ok

======================================================================
ERROR: test_1から10まで数をプリントする (main_test.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/k2works/Projects/hiroshima-arc/re_zero_tdd/dev/20181116/replay/main_test.py", line 56, in test_1から10まで数をプリントする
    execute(10)
TypeError: execute() takes 0 positional arguments but 1 was given

----------------------------------------------------------------------
Ran 5 tests in 0.002s

FAILED (errors=1)

テストおじさん: execute 関数 に引数を追加しないといけませんね。

fizz_buzz_date = {
    'values': [],
    'count': 0
}


def execute(count): (2)
    set_count(count)
    set_values_by_fizz_buzz()
    print_values()


def set_count(count):
    # レンジは指定された値を範囲に含めないので1プラスする
    fizz_buzz_date['count'] = count + 1

テストおじさん: どうかな?

$ python -m unittest main_test.MainTest.test_1から10まで数をプリントする
E
======================================================================
ERROR: test_1から10まで数をプリントする (main_test.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/k2works/Projects/hiroshima-arc/re_zero_tdd/dev/20181116/replay/main_test.py", line 51, in setUp
    execute()
TypeError: execute() missing 1 required positional argument: 'count'

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (errors=1)

テストおじさん: TypeError: execute() missing 1 required positional argument: 'count' ん? execute(10) と明記してるはずだが・・・

テストおじさん: line 51, in setUp がおかしいって?

    def setUp(self):
        with captured_stdout() as stdout:
            execute() (3)
            self.lines = stdout.getvalue().splitlines()

テストおじさん: あーここね。既存でも既存コードの影響もあるし、ここは キーワード引数とデフォルト値 を使いましょう。

キーワード引数は、呼び出す際には省略できる。その場合は、用意されたデフォルト値が引数として使われる。

def execute(count=100): (5)
    set_count(count)
    set_values_by_fizz_buzz()
    print_values()

....

    def test_1から10まで数をプリントする(self):
        with captured_stdout() as stdout:
            execute(count=10) (4)
            self.lines = stdout.getvalue().splitlines()
        self.assertNotIn("11", self.lines)
        self.assertEqual("Buzz", self.lines[10])

テストおじさん: これでどうかな・・・ありゃ。

$ python -m unittest main_test.MainTest -v
test_1から100まで数をプリントできるようにする (main_test.MainTest) ... ok
test_1から10まで数をプリントする (main_test.MainTest) ... FAIL
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (main_test.MainTest) ... ok
test_3の倍数のときは数の代わりにFizzをプリントする (main_test.MainTest) ... ok
test_5の倍数のときはBuzzとプリントする (main_test.MainTest) ... ok

======================================================================
FAIL: test_1から10まで数をプリントする (main_test.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/k2works/Projects/hiroshima-arc/re_zero_tdd/dev/20181116/replay/main_test.py", line 58, in test_1から10まで数をプリントする
    self.assertNotIn("11", self.lines)
AssertionError: '11' unexpectedly found in ['FizzBuzz', '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', '31', '32', 'Fizz', '34', 'Buzz', 'Fizz', '37', '38', 'Fizz', 'Buzz', '41', 'Fizz', '43', '44', 'FizzBuzz', '46', '47', 'Fizz', '49', 'Buzz', 'Fizz', '52', '53', 'Fizz', 'Buzz', '56', 'Fizz', '58', '59', 'FizzBuzz', '61', '62', 'Fizz', '64', 'Buzz', 'Fizz', '67', '68', 'Fizz', 'Buzz', '71', 'Fizz', '73', '74', 'FizzBuzz', '76', '77', 'Fizz', '79', 'Buzz', 'Fizz', '82', '83', 'Fizz', 'Buzz', '86', 'Fizz', '88', '89', 'FizzBuzz', '91', '92', 'Fizz', '94', 'Buzz', 'Fizz', '97', '98', 'Fizz', 'Buzz', 'FizzBuzz', '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', '31', '32', 'Fizz', '34', 'Buzz', 'Fizz', '37', '38', 'Fizz', 'Buzz', '41', 'Fizz', '43', '44', 'FizzBuzz', '46', '47', 'Fizz', '49', 'Buzz', 'Fizz', '52', '53', 'Fizz', 'Buzz', '56', 'Fizz', '58', '59', 'FizzBuzz', '61', '62', 'Fizz', '64', 'Buzz', 'Fizz', '67', '68', 'Fizz', 'Buzz', '71', 'Fizz', '73', '74', 'FizzBuzz', '76', '77', 'Fizz', '79', 'Buzz', 'Fizz', '82', '83', 'Fizz', 'Buzz', '86', 'Fizz', '88', '89', 'FizzBuzz', '91', '92', 'Fizz', '94', 'Buzz', 'Fizz', '97', '98', 'Fizz', 'Buzz', 'FizzBuzz', '1', '2', 'Fizz', '4', 'Buzz', 'Fizz', '7', '8', 'Fizz', 'Buzz']

----------------------------------------------------------------------
Ran 5 tests in 0.002s

FAILED (failures=1)

テストおじさん: あーあ、別のテストの結果も保持してるな。グローバル変数 の初期化処理を追加しないといけませんね。

def set_values_by_fizz_buzz():
    fizz_buzz_date['values'] = [] (6)

    for number in range(fizz_buzz_date['count']):
        value = fizz_buzz(number)
        fizz_buzz_date['values'].append(value)

テストおじさん: 初期化してから値をセットするようにしてと・・・オッケー。

$ python -m unittest main_test.MainTest -v
test_1から100まで数をプリントできるようにする (main_test.MainTest) ... ok
test_1から10まで数をプリントする (main_test.MainTest) ... ok
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (main_test.MainTest) ... ok
test_3の倍数のときは数の代わりにFizzをプリントする (main_test.MainTest) ... ok
test_5の倍数のときはBuzzとプリントする (main_test.MainTest) ... ok

----------------------------------------------------------------------
Ran 5 tests in 0.002s

OK

2.4.13. エラーハンドリング

テストおじさん: 回数を引数で指定できるようにしましたが現行の仕様では1から100までとあるので100より多い場合はプリントしないようにしておきましょう。

テストコードを追加するテストをおじさん。

    def test_100より多い場合はプリントしない(self): (1)
        with captured_stdout() as stdout:
            execute(count=101)
            self.lines = stdout.getvalue().splitlines()
        self.assertNotIn("回数は100までです", self.lines[0])
$ python -m unittest main_test.MainTest -v
test_100より多い場合はプリントしない (main_test.MainTest) ... FAIL
test_1から100まで数をプリントできるようにする (main_test.MainTest) ... ok
test_1から10まで数をプリントする (main_test.MainTest) ... ok
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (main_test.MainTest) ... ok
test_3の倍数のときは数の代わりにFizzをプリントする (main_test.MainTest) ... ok
test_5の倍数のときはBuzzとプリントする (main_test.MainTest) ... ok

======================================================================
FAIL: test_100より多い場合はプリントしない (main_test.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/k2works/Projects/hiroshima-arc/re_zero_tdd/dev/20181116/replay/main_test.py", line 80, in test_100より多い場合はプリントしない
    self.assertEqual("回数は100までです", self.lines[0])
AssertionError: '回数は100までです' != 'FizzBuzz'
- 回数は100までです
+ FizzBuzz


----------------------------------------------------------------------
Ran 6 tests in 0.001s

FAILED (failures=1)

テストおじさん: テストをパスするための 明白な実装 を行います。

def execute(count=100):
    if count < 100: (2)
        set_count(count)
        set_values_by_fizz_buzz()
        print_values()
    else:
        print("回数は100までです")
$ python -m unittest main_test.MainTest -v
test_100より多い場合はプリントしない (main_test.MainTest) ... ok
test_1から100まで数をプリントできるようにする (main_test.MainTest) ... ERROR
test_1から10まで数をプリントする (main_test.MainTest) ... ok
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (main_test.MainTest) ... ERROR
test_3の倍数のときは数の代わりにFizzをプリントする (main_test.MainTest) ... ERROR
test_5の倍数のときはBuzzとプリントする (main_test.MainTest) ... ERROR

======================================================================
ERROR: test_1から100まで数をプリントできるようにする (main_test.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/k2works/Projects/hiroshima-arc/re_zero_tdd/dev/20181116/replay/main_test.py", line 67, in test_1から100まで数をプリントできるようにする
    self.assertEqual("1", self.lines[1])
IndexError: list index out of range

======================================================================
ERROR: test_3と5両方の倍数の場合にはFizzBuzzとプリントする (main_test.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/k2works/Projects/hiroshima-arc/re_zero_tdd/dev/20181116/replay/main_test.py", line 77, in test_3と5両方の倍数の場合にはFizzBuzzとプリントする
    self.assertEqual("FizzBuzz", self.lines[15])
IndexError: list index out of range

======================================================================
ERROR: test_3の倍数のときは数の代わりにFizzをプリントする (main_test.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/k2works/Projects/hiroshima-arc/re_zero_tdd/dev/20181116/replay/main_test.py", line 71, in test_3の倍数のときは数の代わりにFizzをプリントする
    self.assertEqual("Fizz", self.lines[3])
IndexError: list index out of range

======================================================================
ERROR: test_5の倍数のときはBuzzとプリントする (main_test.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/k2works/Projects/hiroshima-arc/re_zero_tdd/dev/20181116/replay/main_test.py", line 74, in test_5の倍数のときはBuzzとプリントする
    self.assertEqual("Buzz", self.lines[5])
IndexError: list index out of range

----------------------------------------------------------------------
Ran 6 tests in 0.001s

FAILED (errors=4)

テストおじさん: ありゃ? あー count < 100 だと100の場合は評価されないわな。

def execute(count=100):
    if count < 100: (2)
        set_count(count)
        set_values_by_fizz_buzz()
        print_values()
    else:
        print("回数は100までです")

テストおじさん: ここはこうかな。

def execute(count=100):
    if count <= 100: (3)
        set_count(count)
        set_values_by_fizz_buzz()
        print_values()
    else:
        print("回数は100までです")
$ python -m unittest main_test.MainTest -v
test_100より多い場合はプリントしない (main_test.MainTest) ... ok
test_1から100まで数をプリントできるようにする (main_test.MainTest) ... ok
test_1から10まで数をプリントする (main_test.MainTest) ... ok
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (main_test.MainTest) ... ok
test_3の倍数のときは数の代わりにFizzをプリントする (main_test.MainTest) ... ok
test_5の倍数のときはBuzzとプリントする (main_test.MainTest) ... ok

----------------------------------------------------------------------
Ran 6 tests in 0.001s

OK

テストおじさん: さてテストはパスしましたが 抽象レベルが正しくないコード を追加してしまいました。

テストおじさん: ここは メソッドの抽出 を実施してします。

テストおじさん: 関数名は 抽象レベルに適切な名前を選ぶ ようにしましょう。

テストおじさん: この場合は 名前で副作用を示すべき 関数名が適切でしょうか。

N2: 抽象レベルに適切な名前を選ぶ

実装をそのまま表すような名前をつけないでください。今、作業しているクラス、関数の抽象レベルを反映した名前を選んでください。

def execute(count=100):
    if count <= 100:
        set_count(count)
        set_values_by_fizz_buzz()
        print_values()
    else:
        print_error_message() (4)


def print_error_message(): (5)
    print("回数は100までです")

テストおじさん: エラーメッセージは変更する可能があるので 定数 にしておきましょう。

ERROR_MSG = "回数は100までです" (6)

...

def print_error_message():
    print(ERROR_MSG)
$ python -m unittest main_test.MainTest -v
test_100より多い場合はプリントしない (main_test.MainTest) ... ok
test_1から100まで数をプリントできるようにする (main_test.MainTest) ... ok
test_1から10まで数をプリントする (main_test.MainTest) ... ok
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (main_test.MainTest) ... ok
test_3の倍数のときは数の代わりにFizzをプリントする (main_test.MainTest) ... ok
test_5の倍数のときはBuzzとプリントする (main_test.MainTest) ... ok

----------------------------------------------------------------------
Ran 6 tests in 0.001s

OK

テストおじさん: あと・・・ 100 というのがぱっと見何を意味するかわかりませんね。

def execute(count=100): (7)
    if count <= 100: (8)
        set_count(count)
        set_values_by_fizz_buzz()
        print_values()
    else:
        print_error_message()

テストおじさん: シンボリック定数によるマジックナンバーの置き換え を実施しましょう。

シンボリック定数によるマジックナンバーの置き換え

特別な意味を持った数字のリテラルがある。

定数を作り、それにふさわしい名前をつけて、そのリテラルを置き換える。

G25: マジックナンバーを名前付けした定数に置き換える

「マジックナンバー」は、数字だけに限りません。それ自身では値の意味も表現できないものすべてに当てはまります。

MAX_COUNT = 100 (9)
ERROR_MSG = "回数は100までです"
BUZZ = "Buzz"
FIZZ = "Fizz"
FIZZ_BUZZ = "FizzBuzz"
fizz_buzz_date = {
    'values': [],
    'count': 0
}


def execute(count=MAX_COUNT): (10)
    if count <= MAX_COUNT: (11)
        set_count(count)
        set_values_by_fizz_buzz()
        print_values()
    else:
        print_error_message()
$ python -m unittest main_test.MainTest -v
test_100より多い場合はプリントしない (main_test.MainTest) ... ok
test_1から100まで数をプリントできるようにする (main_test.MainTest) ... ok
test_1から10まで数をプリントする (main_test.MainTest) ... ok
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (main_test.MainTest) ... ok
test_3の倍数のときは数の代わりにFizzをプリントする (main_test.MainTest) ... ok
test_5の倍数のときはBuzzとプリントする (main_test.MainTest) ... ok

----------------------------------------------------------------------
Ran 6 tests in 0.001s

OK

テストおじさん: メッセージ内の回数も 定数 から参照できるように フォーマット済み文字列リテラル にしておきましょう。

どれが変数でどれが文字列かも一目では判別できません。もう少し、わかりやすく整理された書き方ができれば、こうした問題を回避できます。

このようなときに用いられるのがフォーマット済み文字列リテラルです。これは、文字列リテラルの中に、変数を埋め込んで記述したものです。

MAX_COUNT = 100
ERROR_MSG = f"回数は{MAX_COUNT}までです" (12)

...

    def test_100より多い場合はプリントしない(self):
        with captured_stdout() as stdout:
            execute(count=101)
            self.lines = stdout.getvalue().splitlines()
        self.assertEqual(f"回数は{MAX_COUNT}までです", self.lines[0]) (13)
$ python -m unittest main_test.MainTest -v
test_100より多い場合はプリントしない (main_test.MainTest) ... ok
test_1から100まで数をプリントできるようにする (main_test.MainTest) ... ok
test_1から10まで数をプリントする (main_test.MainTest) ... ok
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (main_test.MainTest) ... ok
test_3の倍数のときは数の代わりにFizzをプリントする (main_test.MainTest) ... ok
test_5の倍数のときはBuzzとプリントする (main_test.MainTest) ... ok

----------------------------------------------------------------------
Ran 6 tests in 0.001s

OK

2.4.14. 条件記述の分解

テストおじさん: さて、いよいよリファクタリングも佳境に入ってきました。

テストおじさん: 仕上げに 条件記述の分解 を実施して完了としましょう。

条件記述の分解

複雑な条件記述(if-then-else)がある。

その条件記述部とthenm部およびelse部から、メソッドを抽出する。

テストおじさん: まずは、 fizz_buzz 関数から。

def fizz_buzz(number):
    value = number

    if number % 3 == 0 and number % 5 == 0:
        value = FIZZ_BUZZ
    elif number % 3 == 0:
        value = FIZZ
    elif number % 5 == 0:
        value = BUZZ

    return value

テストおじさん: クエリーメソッドメソッドの抽出 します。

クエリーメソッド

時には、あるオブジェクトが別のオブジェクトの状態に基づいて、決定をおこなわなくてはならないことがある。 この別のオブジェクトは、おおむね自分自身のためだけに決定を行うはずなので、これは望ましくない。 しかし、オブジェクトがプロトコルの一部として決定基準を提供しなければならないときは、そのメソッドには、"is","was","have"といった接頭辞を付けて、それをおこなう。

def is_fizz(number):
    number % 3 == 0


def fizz_buzz(number):
    value = number

    if number % 3 == 0 and number % 5 == 0:
        value = FIZZ_BUZZ
    elif is_fizz(number): (1)
        value = FIZZ
    elif number % 5 == 0:
        value = BUZZ

    return value

テストおじさん: 確認・・・ありゃ?

$ python -m unittest main_test.MainTest -v
test_100より多い場合はプリントしない (main_test.MainTest) ... ok
test_1から100まで数をプリントできるようにする (main_test.MainTest) ... ok
test_1から10まで数をプリントする (main_test.MainTest) ... ok
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (main_test.MainTest) ... ok
test_3の倍数のときは数の代わりにFizzをプリントする (main_test.MainTest) ... FAIL
test_5の倍数のときはBuzzとプリントする (main_test.MainTest) ... ok

======================================================================
FAIL: test_3の倍数のときは数の代わりにFizzをプリントする (main_test.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/k2works/Projects/hiroshima-arc/re_zero_tdd/dev/20181116/replay/main_test.py", line 81, in test_3の倍数のときは数の代わりにFizzをプリントする
    self.assertEqual("Fizz", self.lines[3])
AssertionError: 'Fizz' != '3'
- Fizz
+ 3


----------------------------------------------------------------------
Ran 6 tests in 0.001s

FAILED (failures=1)

テストおじさん: 変更部分が早速失敗してますね。

テスト初めて書いたひと: is_fizz 関数が結果を返していないですね。

def is_fizz(number):
    number % 3 == 0 (2)


def fizz_buzz(number):
    value = number

    if number % 3 == 0 and number % 5 == 0:
        value = FIZZ_BUZZ
    elif is_fizz(number):
        value = FIZZ
    elif number % 5 == 0:
        value = BUZZ

    return value

テストおじさん: そっすね・・・ここはこうかな。

def is_fizz(number):
    return number % 3 == 0 (3)


def fizz_buzz(number):
    value = number

    if number % 3 == 0 and number % 5 == 0:
        value = FIZZ_BUZZ
    elif is_fizz(number):
        value = FIZZ
    elif number % 5 == 0:
        value = BUZZ

    return value
$ python -m unittest main_test.MainTest -v
test_100より多い場合はプリントしない (main_test.MainTest) ... ok
test_1から100まで数をプリントできるようにする (main_test.MainTest) ... ok
test_1から10まで数をプリントする (main_test.MainTest) ... ok
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (main_test.MainTest) ... ok
test_3の倍数のときは数の代わりにFizzをプリントする (main_test.MainTest) ... ok
test_5の倍数のときはBuzzとプリントする (main_test.MainTest) ... ok

----------------------------------------------------------------------
Ran 6 tests in 0.001s

OK

テストおじさん: オッケー、残りも 明白な実装 で片付けましょう。

def is_buzz(number):
    return number % 5 == 0


def is_fizz_buzz(number):
    return number % 3 == 0 and number % 5 == 0


def fizz_buzz(number):
    value = number

    if is_fizz_buzz(number): (5)
        value = FIZZ_BUZZ
    elif is_fizz(number):
        value = FIZZ
    elif is_buzz(number): (4)
        value = BUZZ

    return value
$ python -m unittest main_test.MainTest -v
test_100より多い場合はプリントしない (main_test.MainTest) ... ok
test_1から100まで数をプリントできるようにする (main_test.MainTest) ... ok
test_1から10まで数をプリントする (main_test.MainTest) ... ok
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (main_test.MainTest) ... ok
test_3の倍数のときは数の代わりにFizzをプリントする (main_test.MainTest) ... ok
test_5の倍数のときはBuzzとプリントする (main_test.MainTest) ... ok

----------------------------------------------------------------------
Ran 6 tests in 0.001s

OK

テストおじさん: おつぎは、 execute 関数です。

def execute(count=MAX_COUNT):
    if count <= MAX_COUNT: (6)
        set_count(count)
        set_values_by_fizz_buzz()
        print_values()
    else:
        print_error_message()

テストおじさん: 先程と同様に 明白な実装クエリーメソッドメソッドの抽出 を実施します。

def execute(count=MAX_COUNT):
    if is_not_max_count(count): (7)
        set_count(count)
        set_values_by_fizz_buzz()
        print_values()
    else:
        print_error_message()


def is_not_max_count(count):
    return count <= MAX_COUNT
$ python -m unittest main_test.MainTest -v
test_100より多い場合はプリントしない (main_test.MainTest) ... ok
test_1から100まで数をプリントできるようにする (main_test.MainTest) ... ok
test_1から10まで数をプリントする (main_test.MainTest) ... ok
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (main_test.MainTest) ... ok
test_3の倍数のときは数の代わりにFizzをプリントする (main_test.MainTest) ... ok
test_5の倍数のときはBuzzとプリントする (main_test.MainTest) ... ok

----------------------------------------------------------------------
Ran 6 tests in 0.001s

OK

リモートさん: MAX_COUNT は含まれるから はっきりした名前 でないですね。

N4: はっきりした名前

関数や変数の働きを的確に表す名前を選びましょう。

テストおじさん: そうですね、ここは MAX_COUNT を含むので・・・

def execute(count=MAX_COUNT):
    if within_max_count(count): (8)
        set_count(count)
        set_values_by_fizz_buzz()
        print_values()
    else:
        print_error_message()


def within_max_count(count):
    return count <= MAX_COUNT
$ python -m unittest main_test.MainTest -v
test_100より多い場合はプリントしない (main_test.MainTest) ... ok
test_1から100まで数をプリントできるようにする (main_test.MainTest) ... ok
test_1から10まで数をプリントする (main_test.MainTest) ... ok
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (main_test.MainTest) ... ok
test_3の倍数のときは数の代わりにFizzをプリントする (main_test.MainTest) ... ok
test_5の倍数のときはBuzzとプリントする (main_test.MainTest) ... ok

----------------------------------------------------------------------
Ran 6 tests in 0.001s

OK

2.4.15. モジュール分割

テストおじさん: リファクタリングも一段落つきました。いよいよ モジュール 分割の時間です。

Pythonでは、ファイルに保存したプログラムを「モジュール」として他のプログラムファイルから読み込んで利用できる。

テストおじさん: 今回は fizz_buzz モジュールとテスト用モジュールに分割します。

main_test.py をコピーして fizz_buzz.py ファイルを作成して編集するテストをおじさん。

fizz_buzz.py

MAX_COUNT = 100
ERROR_MSG = f"回数は{MAX_COUNT}までです"
BUZZ = "Buzz"
FIZZ = "Fizz"
FIZZ_BUZZ = "FizzBuzz"
fizz_buzz_date = {
    'values': [],
    'count': 0
}


def execute(count=MAX_COUNT):
    if within_max_count(count):
        set_count(count)
        set_values_by_fizz_buzz()
        print_values()
    else:
        print_error_message()


def within_max_count(count):
    return count <= MAX_COUNT


def set_count(count):
    # レンジは指定された値を範囲に含めないので1プラスする
    fizz_buzz_date['count'] = count + 1


def set_values_by_fizz_buzz():
    fizz_buzz_date['values'] = []

    for number in range(fizz_buzz_date['count']):
        value = fizz_buzz(number)
        fizz_buzz_date['values'].append(value)


def print_values():
    for value in fizz_buzz_date['values']:
        print(value)


def print_error_message():
    print(ERROR_MSG)


def is_fizz(number):
    return number % 3 == 0


def is_buzz(number):
    return number % 5 == 0


def is_fizz_buzz(number):
    return number % 3 == 0 and number % 5 == 0


def fizz_buzz(number):
    value = number

    if is_fizz_buzz(number):
        value = FIZZ_BUZZ
    elif is_fizz(number):
        value = FIZZ
    elif is_buzz(number):
        value = BUZZ

    return value

テストおじさん: テストモジュール名もふさわしい名前に変更しておきましょう。

main_test.py のファイル名を fizz_buzz_test.py に変えるテストをおじさん。

fizz_buzz_test.py

import unittest
from test.support import captured_stdout


class MainTest(unittest.TestCase):
    def setUp(self):
        with captured_stdout() as stdout:
            execute()
            self.lines = stdout.getvalue().splitlines()

    def test_1から10まで数をプリントする(self):
        with captured_stdout() as stdout:
            execute(count=10)
            self.lines = stdout.getvalue().splitlines()
        self.assertNotIn("11", self.lines)
        self.assertEqual("Buzz", self.lines[10])

    def test_1から100まで数をプリントできるようにする(self):
        self.assertEqual("1", self.lines[1])
        self.assertEqual("Buzz", self.lines[MAX_COUNT])

    def test_3の倍数のときは数の代わりにFizzをプリントする(self):
        self.assertEqual("Fizz", self.lines[3])

    def test_5の倍数のときはBuzzとプリントする(self):
        self.assertEqual("Buzz", self.lines[5])

    def test_3と5両方の倍数の場合にはFizzBuzzとプリントする(self):
        self.assertEqual("FizzBuzz", self.lines[15])

    def test_100より多い場合はプリントしない(self):
        with captured_stdout() as stdout:
            execute(count=101)
            self.lines = stdout.getvalue().splitlines()
        self.assertEqual(f"回数は{MAX_COUNT}までです", self.lines[0])


class RangeTest(unittest.TestCase):
    def test_範囲を示すレンジ(self):
        rg = range(10)
        self.assertEqual(0, rg[0])
        self.assertEqual(9, rg[-1])
        rg = range(5, 10)
        self.assertEqual(5, rg[0])
        self.assertEqual(9, rg[-1])
        rg = range(10, 20, 2)
        self.assertEqual(12, rg[1])
        self.assertEqual(18, rg[-1])

    def test_レンジのシーケンス演算(self):
        rg = range(30)
        self.assertTrue(10 in rg)
        self.assertTrue(30 not in rg)
        rg = range(50)
        self.assertEqual(range(1, 4), rg[1:4])
        self.assertEqual(50, len(rg))
        self.assertEqual(49, max(rg))
        self.assertEqual(0, min(rg))

    def test_シーケンス間の変換(self):
        list1 = [MAX_COUNT, 200, 300]
        tpl1 = (123, 'ok', True)
        rng1 = range(10, 20)
        result = list1 + list(tpl1) + list(rng1)
        self.assertEqual([MAX_COUNT, 200, 300, 123, 'ok', True, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19], result)


if __name__ == "__main__":
    unittest.main()

テストおじさん: テストが動くか確認しておきましょう。

$ python fizz_buzz_test.py -v
test_100より多い場合はプリントしない (__main__.MainTest) ... ERROR
test_1から100まで数をプリントできるようにする (__main__.MainTest) ... ERROR
test_1から10まで数をプリントする (__main__.MainTest) ... ERROR
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (__main__.MainTest) ... ERROR
test_3の倍数のときは数の代わりにFizzをプリントする (__main__.MainTest) ... ERROR
test_5の倍数のときはBuzzとプリントする (__main__.MainTest) ... ERROR
test_シーケンス間の変換 (__main__.RangeTest) ... ERROR
test_レンジのシーケンス演算 (__main__.RangeTest) ... ok
test_範囲を示すレンジ (__main__.RangeTest) ... ok

======================================================================
ERROR: test_100より多い場合はプリントしない (__main__.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "fizz_buzz_test.py", line 8, in setUp
    execute()
NameError: name 'execute' is not defined

======================================================================
ERROR: test_1から100まで数をプリントできるようにする (__main__.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "fizz_buzz_test.py", line 8, in setUp
    execute()
NameError: name 'execute' is not defined

======================================================================
ERROR: test_1から10まで数をプリントする (__main__.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "fizz_buzz_test.py", line 8, in setUp
    execute()
NameError: name 'execute' is not defined

======================================================================
ERROR: test_3と5両方の倍数の場合にはFizzBuzzとプリントする (__main__.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "fizz_buzz_test.py", line 8, in setUp
    execute()
NameError: name 'execute' is not defined

======================================================================
ERROR: test_3の倍数のときは数の代わりにFizzをプリントする (__main__.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "fizz_buzz_test.py", line 8, in setUp
    execute()
NameError: name 'execute' is not defined

======================================================================
ERROR: test_5の倍数のときはBuzzとプリントする (__main__.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "fizz_buzz_test.py", line 8, in setUp
    execute()
NameError: name 'execute' is not defined

======================================================================
ERROR: test_シーケンス間の変換 (__main__.RangeTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "fizz_buzz_test.py", line 61, in test_シーケンス間の変換
    list1 = [MAX_COUNT, 200, 300]
NameError: name 'MAX_COUNT' is not defined

----------------------------------------------------------------------
Ran 9 tests in 0.001s

FAILED (errors=7)

テストおじさん: まあ、そうでしょうね。なになに NameError: name 'execute' is not defined ・・・ 分離した fizz_buzz モジュールを インポート してませんね。

importは、指定したモジュールを読み込み、使える状態にします。

import unittest
from test.support import captured_stdout
from fizz_buzz import execute (1)

テストおじさん: fizz_buzz モジュールから execute 関数インポート すると・・・ありゃ?

$ python fizz_buzz_test.py -v
test_100より多い場合はプリントしない (__main__.MainTest) ... ERROR
test_1から100まで数をプリントできるようにする (__main__.MainTest) ... ERROR
test_1から10まで数をプリントする (__main__.MainTest) ... ok
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (__main__.MainTest) ... ok
test_3の倍数のときは数の代わりにFizzをプリントする (__main__.MainTest) ... ok
test_5の倍数のときはBuzzとプリントする (__main__.MainTest) ... ok
test_シーケンス間の変換 (__main__.RangeTest) ... ERROR
test_レンジのシーケンス演算 (__main__.RangeTest) ... ok
test_範囲を示すレンジ (__main__.RangeTest) ... ok

======================================================================
ERROR: test_100より多い場合はプリントしない (__main__.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "fizz_buzz_test.py", line 35, in test_100より多い場合はプリントしない
    self.assertEqual(f"回数は{MAX_COUNT}までです", self.lines[0])
NameError: name 'MAX_COUNT' is not defined

======================================================================
ERROR: test_1から100まで数をプリントできるようにする (__main__.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "fizz_buzz_test.py", line 20, in test_1から100まで数をプリントできるようにする
    self.assertEqual("Buzz", self.lines[MAX_COUNT])
NameError: name 'MAX_COUNT' is not defined

======================================================================
ERROR: test_シーケンス間の変換 (__main__.RangeTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "fizz_buzz_test.py", line 61, in test_シーケンス間の変換
    list1 = [MAX_COUNT, 200, 300]
NameError: name 'MAX_COUNT' is not defined

----------------------------------------------------------------------
Ran 9 tests in 0.001s

FAILED (errors=3)

テストおじさん: NameError: name 'MAX_COUNT' is not defined ・・・あー、 fizz_buzz モジュール から 定数 MAX_COUNTインポート しないとけないのね。

import unittest
from test.support import captured_stdout
from fizz_buzz import execute, MAX_COUNT (2)

テストおじさん: これでいいかな・・・オッケー!

$ python fizz_buzz_test.py -v
test_100より多い場合はプリントしない (__main__.MainTest) ... ok
test_1から100まで数をプリントできるようにする (__main__.MainTest) ... ok
test_1から10まで数をプリントする (__main__.MainTest) ... ok
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (__main__.MainTest) ... ok
test_3の倍数のときは数の代わりにFizzをプリントする (__main__.MainTest) ... ok
test_5の倍数のときはBuzzとプリントする (__main__.MainTest) ... ok
test_シーケンス間の変換 (__main__.RangeTest) ... ok
test_レンジのシーケンス演算 (__main__.RangeTest) ... ok
test_範囲を示すレンジ (__main__.RangeTest) ... ok

----------------------------------------------------------------------
Ran 9 tests in 0.002s

OK

2.4.16. 名前重要

fizz_buzz モジュールを眺めるテストおじさん。

MAX_COUNT = 100
ERROR_MSG = f"回数は{MAX_COUNT}までです"
BUZZ = "Buzz"
FIZZ = "Fizz"
FIZZ_BUZZ = "FizzBuzz"
fizz_buzz_date = {   (1)
    'values': [],
    'count': 0
}


def execute(count=MAX_COUNT):
    if within_max_count(count):
        set_count(count)
        set_values_by_fizz_buzz() (2)
        print_values()
    else:
        print_error_message()


def within_max_count(count):
    return count <= MAX_COUNT


def set_count(count):
    # レンジは指定された値を範囲に含めないので1プラスする
    fizz_buzz_date['count'] = count + 1 (3)


def set_values_by_fizz_buzz(): (4)
    fizz_buzz_date['values'] = [] (5)

    for number in range(fizz_buzz_date['count']):
        value = fizz_buzz(number) (6)
        fizz_buzz_date['values'].append(value) (7)


def print_values():
    for value in fizz_buzz_date['values']: (8)
        print(value)


def print_error_message():
    print(ERROR_MSG)


def fizz_buzz(number): (9)
    value = number

    if is_fizz_buzz(number):
        value = FIZZ_BUZZ
    elif is_fizz(number):
        value = FIZZ
    elif is_buzz(number):
        value = BUZZ

    return value


def is_fizz(number):
    return number % 3 == 0


def is_buzz(number):
    return number % 5 == 0


def is_fizz_buzz(number):
    return number % 3 == 0 and number % 5 == 0

テストおじさん: fizz_buzz という関心事のモジュール内の fizz_buzz という単語は冗長というか 抽出レベルに適切な名前を選ぶ に即していない気がします。

テストおじさん: 名前を変更しましょう。

テストおじさん: fizz_buzz_date は・・・あ、これ typo ですね。 fizz_buzz_datafizz_buzz モジュールの data だからこう。

data = { (1)
    'values': [],
    'count': 0
}

...

def set_count(count):
    # レンジは指定された値を範囲に含めないので1プラスする
    data['count'] = count + 1 (3)


def set_values_by_fizz_buzz():
    data['values'] = [] (5)

    for number in range(data['count']):
        value = fizz_buzz(number)
        data['values'].append(value) (7)


def print_values():
    for value in data['values']: (8)
        print(value)
$ python -m unittest fizz_buzz_test.MainTest -v
test_100より多い場合はプリントしない (fizz_buzz_test.MainTest) ... ok
test_1から100まで数をプリントできるようにする (fizz_buzz_test.MainTest) ... ok
test_1から10まで数をプリントする (fizz_buzz_test.MainTest) ... ok
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (fizz_buzz_test.MainTest) ... ok
test_3の倍数のときは数の代わりにFizzをプリントする (fizz_buzz_test.MainTest) ... ok
test_5の倍数のときはBuzzとプリントする (fizz_buzz_test.MainTest) ... ok

----------------------------------------------------------------------
Ran 6 tests in 0.001s

OK

テストおじさん: fizz_buzz 関数は fizz_buzz モジュール内で生成されているから。

def execute(count=MAX_COUNT):
    if within_max_count(count):
        set_count(count)
        set_values_by_generate() (2)
        print_values()
    else:
        print_error_message()

...

def set_values_by_generate(): (4)
    data['values'] = []

    for number in range(data['count']):
        value = generate(number) (6)
        data['values'].append(value)

...

def generate(number): (9)
    value = number

    if is_fizz_buzz(number):
        value = FIZZ_BUZZ
    elif is_fizz(number):
        value = FIZZ
    elif is_buzz(number):
        value = BUZZ

    return value
$ python -m unittest fizz_buzz_test.MainTest -v
test_100より多い場合はプリントしない (fizz_buzz_test.MainTest) ... ok
test_1から100まで数をプリントできるようにする (fizz_buzz_test.MainTest) ... ok
test_1から10まで数をプリントする (fizz_buzz_test.MainTest) ... ok
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (fizz_buzz_test.MainTest) ... ok
test_3の倍数のときは数の代わりにFizzをプリントする (fizz_buzz_test.MainTest) ... ok
test_5の倍数のときはBuzzとプリントする (fizz_buzz_test.MainTest) ... ok

----------------------------------------------------------------------
Ran 6 tests in 0.001s

OK

テストおじさん: execute 関数を見てください。

def execute(count=MAX_COUNT):
    if within_max_count(count):
        set_count(count)
        set_values_by_generate()
        print_values()
    else:
        print_error_message()

テストおじさん: execute 関数だけで処理の概要を読み上げる事ができますね。

  • fizz_buzz モジュールの

    • execute 実行関数は

      • if within_max_count もし、最大カウント以内なら

        • set_count カウントを設定して

        • set_values_by_generate 生成関数によって値を設定して

        • print_values 値をプリントします

      • else そうでなければ

        • print_error_message エラーメッセージをプリントします

テストおじさん: コードの 書式化 は重要です。

書式化の目的

最初にはっきりさせておきましょう。コードの書式化は重要なことです。 大変重要なので無視してはいけませんし、宗教論争と片付けてもいけません。 コードの書式化とは情報伝達を意味し、情報伝達はプロの開発者の仕事を進める上で最も重要なことなのです。

テストおじさん: ソースファイルを 新聞にたとえる メタファーがあります。

新聞にたとえる

ソースファイルも、新聞の記事のようにしたいものです。名前は単純でありながら、説明的でなければなりません。 名前だけを見れば、そのモジュールが自分が見たいモジュールかどうか判断できなければなりません。 ソースファイルの一番最初には、高レベルの概念とアルゴリズムが書かれているべきです。 下へと読み進むに従い、詳細度は増していき、ソースファイルの一番下に達すると、最も低レベルの関数と詳細な記述を目にすることになります。

テストおじさん: あと、気になったのはテストメッセージですが。

$ python -m unittest fizz_buzz_test.MainTest -v
test_100より多い場合はプリントしない (fizz_buzz_test.MainTest) ... ok
test_1から100まで数をプリントできるようにする (fizz_buzz_test.MainTest) ... ok (1)
test_1から10まで数をプリントする (fizz_buzz_test.MainTest) ... ok
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (fizz_buzz_test.MainTest) ... ok
test_3の倍数のときは数の代わりにFizzをプリントする (fizz_buzz_test.MainTest) ... ok
test_5の倍数のときはBuzzとプリントする (fizz_buzz_test.MainTest) ... ok

----------------------------------------------------------------------
Ran 6 tests in 0.001s

OK

テストおじさん: できるようにする では要求になってしまうのでここは仕様だから。

    def test_1から100まで数をプリントする(self): (2)
        self.assertEqual("1", self.lines[1])
        self.assertEqual("Buzz", self.lines[MAX_COUNT])
$ python -m unittest fizz_buzz_test.MainTest -v
test_100より多い場合はプリントしない (fizz_buzz_test.MainTest) ... ok
test_1から100まで数をプリントする (fizz_buzz_test.MainTest) ... ok (2)
test_1から10まで数をプリントする (fizz_buzz_test.MainTest) ... ok
test_3と5両方の倍数の場合にはFizzBuzzとプリントする (fizz_buzz_test.MainTest) ... ok
test_3の倍数のときは数の代わりにFizzをプリントする (fizz_buzz_test.MainTest) ... ok
test_5の倍数のときはBuzzとプリントする (fizz_buzz_test.MainTest) ... ok

----------------------------------------------------------------------
Ran 6 tests in 0.001s

OK

テストおじさん: ですね。

テストおじさん: 名前重要 です。

名前重要

適切な名前をつけられると言うことは、その機能が正しく理解されて、設計されているということで、逆にふさわしい名前がつけられないということは、その機能が果たすべき役割を設計者自身も十分理解できていないということなのではないでしょうか。 個人的には適切な名前をつけることができた機能については、その設計の8割が完成したと考えても言い過ぎでないことが多いように思います。

2.4.17. 構造化プログラミング

テストおじさん: さて、今回は3つのプログラミングパラダイムの一つである 構造化プログラミング アプローチで実装しました。

構造化プログラミング

最初に導入された(最初に誕生したわけではない)パラダイムは、1968年にEdsger Wybe Dijkstraが発見した「構造化プログラミング」である。 Dijkstraは、制限のないジャンプ(goto文の使用)がプログラムの構造に対して有害であることを示した。 今後の章で示すことになるが、彼はこのようなジャンプを、なじみのあるif/then/elseやdo/while/untilといった構文に置き換えた。

構造化プログラミングのパラダイムは、以下のように要約できる。

構造化プログラミングは、直接的な制御の移行に規律を課すものである。

テストおじさん: まず、 テストファーストから始めるアーキテクチャ からプログラミングの基本要素である 基本のデータ 変数 演算 制御構文 を使って以下のコードを実装しました。

基本のデータ

Pythonには多数の型(タイプ)がある。 もっとも重要でよく使われるのは、「int」 「float」「bool」「str」の4つ。

変数

変数は、値を保管するための入れ物。=演算子を使い、値を代入すると自動的に作成される。

演算

算術演算は、+,-,*,/,//,%といった演算子を使って式を書き実行する。

xや÷などは使えないので注意!

制御構文

プログラムの処理の流れを制御するために用意されているのが「制御構文」です。

def execute():
    n = 100
    while n != 0:
        if n % 3 == 0 and n % 5 == 0:
            print("FizzBuzz")
        elif n % 3 == 0:
            print("Fizz")
        elif n % 5 == 0:
            print("Buzz")
        else:
            print(n)
        n = n - 1

テストおじさん: 続いて、 学習用テスト から データ構造 の使い方を学習して以下のコードにリファクタリングしました。

データ構造

Pythonには多数の値をまとめて管理する「コンテナ」が用意されています。 複雑なデータを扱うときに役立ちます。

def execute():
    for n in range(101):
        if n % 3 == 0 and n % 5 == 0:
            print("FizzBuzz")
        elif n % 3 == 0:
            print("Fizz")
        elif n % 5 == 0:
            print("Buzz")
        else:
            print(n)

テストおじさん: 上記の2つのコードにはあらゆるプログラムを構築できる制御構造の最小セット 順次 選択 反復 が含まれています。

順序構造

diag 1121ef9491e0baf4b3878520069a3419

選択構造

diag 50d19e07abe56d1ff4bee77a355f1d9e

反復構造

diag 93fc39fd4a189bcdcd90335935eb7ee5

テストおじさん: さらに、 条件記述の分解 の結果、以下の特徴をコードに見出すことができました。

  • 単純な文を組み合わせた文を1つの抽象文としてみなす

def fizz_buzz(number):
    value = number

    if is_fizz_buzz(number):
        value = FIZZ_BUZZ
    elif is_fizz(number):
        value = FIZZ
    elif is_buzz(number):
        value = BUZZ

    return value
  • 抽象化した文を組み合わせた文をさらに1つの抽象文としてみなす(階層化する)

def execute(count=MAX_COUNT):
    if within_max_count(count):
        set_count(count)
        set_values_by_fizz_buzz()
        print_values()
    else:
        print_error_message()
  • データも抽象化する

MAX_COUNT = 100
ERROR_MSG = f"回数は{MAX_COUNT}までです"
BUZZ = "Buzz"
FIZZ = "Fizz"
FIZZ_BUZZ = "FizzBuzz"
fizz_buzz_data = {
    'values': [],
    'count': 0
}
  • 抽象データとそれを扱う抽象文を他の部分と分離する

fizz_buzz_data = {
    'values': [],
    'count': 0
}

...

def set_values_by_generate():
    fizz_buzz_data['values'] = []

    for number in range(fizz_buzz_data['count']):
        value = fizz_buzz(number)
        fizz_buzz_data['values'].append(value)

テストおじさん: そして、モジュール分割 により 機能分割 を実現しました。

機能分割

構造化プログラミングは、モジュールを証明可能な単位に再帰的に分割することを可能にする。 それによって、モジュールは機能的に分割できる。つまり、大きな問題は上位のレベルの機能に分割できるというわけだ。 分割された機能は、さらに下位レベルの機能へと無限に分割していくことができる。 また、このように分割された機能は、構造化プログラミングの制限された制御構造を使って表現することができる。

テストおじさん: 一方で ロジックとデータの一体化構造化プログラミング アプローチで実現するのは簡単ではなさそうです。

ロジックとデータの一体化

結果局所化の原則から必然的に導き出されるもう一つの原則は、ロジックとデータを一緒にするという原則だ。 ロジックと、ロジックが操作するデータは互いに近くに置くようにしよう。可能であれば同じメソッドか同じオブジェクトに、少なくとも同じパッケージ内に置くようにしよう。 変更を行う際には、ロジックとデータは同じタイミングで変更しなければならないことが多い。 これらが同じ場所にあれば、変更の結果も局所化が保てるだろう。

テストおじさん: オブジェクト指向プログラミング の出番が来たようです。

オブジェクト指向プログラミング

次に導入されたパラダイムは、構造化プログラミングの2年前の1966年に、Ole Johan Dahl と Kristen Nygaardが発見した「オブジェクト指向プログラミング」である。 この2人のプログラマは、ALGOL言語の関数呼び出しのスタックフレームをヒープに移動できること、そのことにより、関数から戻ってきたあとでも関数で宣言したローカル変数が存在し続けられることに気づいた。 この関数はクラスのコンストラクタになり、ローカル変数はインスタンス変数になった。そして、ネストした関数はメソッドになった。 その後、規律のある関数ポインタの使用によって、必然的にポリモーフィズムの発見につながった。

オブジェクト指向プログラミングのパラダイムは、以下のように要約できる。

オブジェクト指向プログラミングは、間接的な制御の移行に規律を課すものである。

3. ふりかえり

# 20181116 セッションリプレイ

## 概要

FizzBuzz をクラスを使わずにテスト駆動で実装する。

### 参加メンバー

- テストおじさん
- テスト鬼さん
- テスト初めて書いたひと

## 仕様

> 1 から 100 までの数をプリントするプログラムを書け。
> ただし 3 の倍数のときは数の代わりに「Fizz」と、5 の倍数のときは「Buzz」とプリントし、3 と 5 両方の倍数の場合には「FizzBuzz」とプリントすること。

## 設計

### TODO リスト

- ~~1 から 100 まで数をプリントできるようにする。~~
- ~~3 の倍数のときは数の代わりに「Fizz」をプリントできるようにする。~~
- ~~5 の倍数のときは「Buzz」とプリントできるようにする。~~
- ~~3 と 5 両方の倍数の場合には「FizzBuzz」とプリントできるようにする。~~

## 開発

### ふりかえり

#### Keep

- TODO リスト
- テスファースト・アサートファースト
- コードの不吉な臭い
- ベイビーステップ
- 機能分割
- テストの自動化

#### Problem

- 構造化プログラミング
- ロジックとデータの一体化

#### Try

- オブジェクト指向プログラミング

## 参照

- [どうしてプログラマに・・・プログラムが書けないのか?](http://www.aoky.net/articles/jeff_atwood/why_cant_programmers_program.htm)
- [Python プログラムの標準出力をテストする](https://qiita.com/Asayu123/items/6f2471aa5ebe597b2638)
MAX_COUNT = 100
ERROR_MSG = f"回数は{MAX_COUNT}までです"
BUZZ = "Buzz"
FIZZ = "Fizz"
FIZZ_BUZZ = "FizzBuzz"
data = {
    'values': [],
    'count': 0
}


def execute(count=MAX_COUNT):
    if within_max_count(count):
        set_count(count)
        set_values_by_generate()
        print_values()
    else:
        print_error_message()


def within_max_count(count):
    return count <= MAX_COUNT


def set_count(count):
    # レンジは指定された値を範囲に含めないので1プラスする
    data['count'] = count + 1


def set_values_by_generate():
    data['values'] = []

    for number in range(data['count']):
        value = generate(number)
        data['values'].append(value)


def print_values():
    for value in data['values']:
        print(value)


def print_error_message():
    print(ERROR_MSG)


def generate(number):
    value = number

    if is_fizz_buzz(number):
        value = FIZZ_BUZZ
    elif is_fizz(number):
        value = FIZZ
    elif is_buzz(number):
        value = BUZZ

    return value


def is_fizz(number):
    return number % 3 == 0


def is_buzz(number):
    return number % 5 == 0


def is_fizz_buzz(number):
    return number % 3 == 0 and number % 5 == 0
import unittest
from test.support import captured_stdout
from fizz_buzz import execute, MAX_COUNT


class MainTest(unittest.TestCase):
    def setUp(self):
        with captured_stdout() as stdout:
            execute()
            self.lines = stdout.getvalue().splitlines()

    def test_1から10まで数をプリントする(self):
        with captured_stdout() as stdout:
            execute(count=10)
            self.lines = stdout.getvalue().splitlines()
        self.assertNotIn("11", self.lines)
        self.assertEqual("Buzz", self.lines[10])

    def test_1から100まで数をプリントする(self):
        self.assertEqual("1", self.lines[1])
        self.assertEqual("Buzz", self.lines[MAX_COUNT])

    def test_3の倍数のときは数の代わりにFizzをプリントする(self):
        self.assertEqual("Fizz", self.lines[3])

    def test_5の倍数のときはBuzzとプリントする(self):
        self.assertEqual("Buzz", self.lines[5])

    def test_3と5両方の倍数の場合にはFizzBuzzとプリントする(self):
        self.assertEqual("FizzBuzz", self.lines[15])

    def test_100より多い場合はプリントしない(self):
        with captured_stdout() as stdout:
            execute(count=101)
            self.lines = stdout.getvalue().splitlines()
        self.assertEqual(f"回数は{MAX_COUNT}までです", self.lines[0])


class RangeTest(unittest.TestCase):
    def test_範囲を示すレンジ(self):
        rg = range(10)
        self.assertEqual(0, rg[0])
        self.assertEqual(9, rg[-1])
        rg = range(5, 10)
        self.assertEqual(5, rg[0])
        self.assertEqual(9, rg[-1])
        rg = range(10, 20, 2)
        self.assertEqual(12, rg[1])
        self.assertEqual(18, rg[-1])

    def test_レンジのシーケンス演算(self):
        rg = range(30)
        self.assertTrue(10 in rg)
        self.assertTrue(30 not in rg)
        rg = range(50)
        self.assertEqual(range(1, 4), rg[1:4])
        self.assertEqual(50, len(rg))
        self.assertEqual(49, max(rg))
        self.assertEqual(0, min(rg))

    def test_シーケンス間の変換(self):
        list1 = [MAX_COUNT, 200, 300]
        tpl1 = (123, 'ok', True)
        rng1 = range(10, 20)
        result = list1 + list(tpl1) + list(rng1)
        self.assertEqual([MAX_COUNT, 200, 300, 123, 'ok', True, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19], result)


if __name__ == "__main__":
    unittest.main()

3.1. Keep

テストおじさん: リファクタリングのヒントからチェック内容を確認しましょう。

テストおじさん: まず TODOリスト を作成して テスファースト アサートファースト でリファクタリングに入る前の準備をしましょう。

  • ❏ 構造的に機能を付け加えにくいプログラムに、新規機能を追加しなければならない場合には、まず機能追加が簡単になるようにリファクタリングをしてから追加を行うこと。

  • リファクタリングに入る前に、しっかりとした一連のテスト群が用意できているかを確認すること。これらのテストには自己診断機能が不可欠である。

  • ✓ リファクタリングでは小さなステップでプログラムを変更していく。そのため、誤ったことしても、バグを見つけるのは簡単である。

  • ✓ コンパイラが理解出るコードは誰にでも書ける。すぐれたプログラマは、人間にとってわかりやすいコードを書く。

  • ❏ リファクタリング(名詞):外側から見たときの振る舞いを保ちつつ、理解や修正が簡単になるように、ソフトウェアの内部構造を変化させること。

  • ❏ リファクタリングする(動詞):一連のリファクタリングを適用して、外部から見た振る舞いの変更なしに、ソフトウェアを再構築すること。

  • ✓ 3三度目になったらリファクタリング開始。

  • ❏ あまり早期にインタフェースを公開しないこと。スムーズなリファクタリングのために、時にはコードの所有権のポリシーを変えることも必要。

  • ✓ コメントの必要を感じたときにはリファクタリングを行って、コメントを書かなくとも内容がわかるようなコードを目指すこと。

  • ❏ テストを完全に自動化して、その結果もテストにチェックさせること。

  • ✓ テストをひとそろいにしておくと、バグの検出に絶大な威力を発揮する。これによって、バグの発見にかかる時間は削除される。

テストおじさん: 次にリファクタリングにあたって コードの不吉な臭い である 重複したコード をリファクタリングしました。

  • ❏ 構造的に機能を付け加えにくいプログラムに、新規機能を追加しなければならない場合には、まず機能追加が簡単になるようにリファクタリングをしてから追加を行うこと。

  • ✓ リファクタリングでは小さなステップでプログラムを変更していく。そのため、誤ったことしても、バグを見つけるのは簡単である。

  • ✓ コンパイラが理解出るコードは誰にでも書ける。すぐれたプログラマは、人間にとってわかりやすいコードを書く。

  • ❏ リファクタリング(名詞):外側から見たときの振る舞いを保ちつつ、理解や修正が簡単になるように、ソフトウェアの内部構造を変化させること。

  • ❏ リファクタリングする(動詞):一連のリファクタリングを適用して、外部から見た振る舞いの変更なしに、ソフトウェアを再構築すること。

  • 3三度目になったらリファクタリング開始。

  • ❏ あまり早期にインタフェースを公開しないこと。スムーズなリファクタリングのために、時にはコードの所有権のポリシーを変えることも必要。

  • ✓ コメントの必要を感じたときにはリファクタリングを行って、コメントを書かなくとも内容がわかるようなコードを目指すこと。

  • ❏ テストを完全に自動化して、その結果もテストにチェックさせること。

  • ✓ テストをひとそろいにしておくと、バグの検出に絶大な威力を発揮する。これによって、バグの発見にかかる時間は削除される。

テストおじさん: そして、ベイビーステップ で人間にとってわかりやすいコードにリファクタリングしました。

  • ❏ 構造的に機能を付け加えにくいプログラムに、新規機能を追加しなければならない場合には、まず機能追加が簡単になるようにリファクタリングをしてから追加を行うこと。

  • リファクタリングでは小さなステップでプログラムを変更していく。そのため、誤ったことしても、バグを見つけるのは簡単である。

  • コンパイラが理解出るコードは誰にでも書ける。すぐれたプログラマは、人間にとってわかりやすいコードを書く。

  • ❏ リファクタリング(名詞):外側から見たときの振る舞いを保ちつつ、理解や修正が簡単になるように、ソフトウェアの内部構造を変化させること。

  • ❏ リファクタリングする(動詞):一連のリファクタリングを適用して、外部から見た振る舞いの変更なしに、ソフトウェアを再構築すること。

  • ❏ あまり早期にインタフェースを公開しないこと。スムーズなリファクタリングのために、時にはコードの所有権のポリシーを変えることも必要。

  • コメントの必要を感じたときにはリファクタリングを行って、コメントを書かなくとも内容がわかるようなコードを目指すこと。

  • ❏ テストを完全に自動化して、その結果もテストにチェックさせること。

  • テストをひとそろいにしておくと、バグの検出に絶大な威力を発揮する。これによって、バグの発見にかかる時間は削除される。

テストおじさん: 最後にモジュール分割リファクタリングにより 構造化プログラミング における 機能分割 を実現しました。

  • ❏ 構造的に機能を付け加えにくいプログラムに、新規機能を追加しなければならない場合には、まず機能追加が簡単になるようにリファクタリングをしてから追加を行うこと。

  • リファクタリング(名詞):外側から見たときの振る舞いを保ちつつ、理解や修正が簡単になるように、ソフトウェアの内部構造を変化させること。

  • リファクタリングする(動詞):一連のリファクタリングを適用して、外部から見た振る舞いの変更なしに、ソフトウェアを再構築すること。

  • ❏ あまり早期にインタフェースを公開しないこと。スムーズなリファクタリングのために、時にはコードの所有権のポリシーを変えることも必要。

  • ❏ テストを完全に自動化して、その結果もテストにチェックさせること。

テストおじさん: CI環境 は導入済みなので本日のテストを反映しておきましょう。

  • ❏ 構造的に機能を付け加えにくいプログラムに、新規機能を追加しなければならない場合には、まず機能追加が簡単になるようにリファクタリングをしてから追加を行うこと。

  • ❏ あまり早期にインタフェースを公開しないこと。スムーズなリファクタリングのために、時にはコードの所有権のポリシーを変えることも必要。

  • テストを完全に自動化して、その結果もテストにチェックさせること。

3.2. Problem

テストおじさん: 構造化プログラミング アプローチでは ロジックとデータの一体化 を実現するのが簡単では無いことがわかりました。

3.3. Try

テストおじさん: 上記の問題を オブジェクト指向 アプローチで解決してみたいと思います。

テストおじさん: 皆さんお疲れ様でした。

テストおじさん以外のメンバー: ありがとうございました。

4. 参照