テスト駆動開発入門

テスト駆動開発とは

これまでは、まずコードを実装し、その後意図した通りに動いているかチェックしていたと思う。しかし、それだとチェックの抜け漏れが発生したり、リファクタリングで意図せずコードの振る舞いが変わってしまったりして、品質が低下してしまう可能性がある。

そこで、しばしば、品質をプロセスに組み込む方法としてテスト駆動開発が用いられる。テスト駆動開発ではまずテストコード(コードをチェックするコード)を書き、その後で実際にコード(テストコードと区別して以下「プロダクトコード」と呼ぶ)を実装する。より具体的には、以下の順序で開発を進めていく。

  • 準備 これから実装するプロダクトコードの振る舞いを整理、箇条書きして、振る舞いリストを作成する。
  • レッド 振る舞いリストの中から1つ選び、テストコードを書き、実行して失敗させる。
  • グリーン 失敗しているテストが成功するようにプロダクトコードを実装する。
  • リファクタリング テストが成功している状態を保ちながら、重複を削除し、テストコードやプロダクトコードをきれいにする。
  • フィードバック 開発を進めていくうちに得られた気づきを、振る舞いリストに反映する。
  • 繰り返す レッドに戻って、さらに開発を進める。
  • テスト駆動開発には、以下に挙げるようなメリットがある。

  • 生きたドキュメント(仕様書)になる テストを読むことでそのモジュールがどのように使われることを想定して作られたかがわかり、かつ、その動作が保証されている。
  • 安心してリファクタリングができる リファクタリング後にテストを実施することで、プロダクトコードの振る舞いが変更されていないことを保証でき、安心してリファクタリングできる。
  • 設計が改善できる テストを先に書くことで、テストしやすい設計(責務が明確、結合度が低い、凝集度が高い、決定的な動作など)を意識するようになり、設計の改善につながる。
  • テストコードは、準備(Arrange)、実行(Act)、検証(Assert)の3段階で構成される(AAAパターン)。テストコードの実装する際には、ゴールを明確にするため、検証、実行、準備の順で記述するとわかりやすい。例として、Stackクラスのsizeメソッドのテストコードは以下に示す。

    def test_sample():
        stack = Stack() # 準備(Arrange)
        stack.push('aaa') # 実行(Act)
        assert stack.size() == 1 # 検証(Assert)

    実装がシンプルな場合には、準備、実行、検証を一行でまとめて記述しても良い。

    def test_sample():
        assert stack(['aaa', 'bbb', 'ccc']).size() == 3

    テストコードが複雑だと感じるときは、扱いにくいモジュールを作ってしまっている場合が多く、テストコードがシンプルになるようにプロダクトコードの設計を見直した方が良い。

    テスト駆動開発を体験する(準備編)

    まずテスト駆動開発の準備として、テストの実行方法、およびテスト成功時と失敗時の挙動を確認しておく。

  • 作業用ディレクトリ「venv_sample」を作成し、VSCodeで作業用ディレクトリを開く
  • 以下のようなファイル構成で設定ファイル群を作成していく

    venv_sample
        ├ src
        |  ├ __init__.py
        |  └ sample.py
        └ tests
           ├ __init__.py
           └ test_sample.py

  • 「venv_sample」の配下に以下のファイル群を作成する
  • # src/__init__.py
    # src/sample.py
    
    def add(a, b):
        return a + b
    # tests/__init__.py
    # tests/test_sample.py
    
    from src import sample
    
    def test_add():
        assert sample.add(1, 2) == 3

  • コマンドパレットから "Python: Configure Tests" を選択
  • Notion Image
    Notion Image

  • pytestを選択
  • Notion Image

  • testsディレクトリを選択
  • Notion Image

  • サイドバーのテスト用アイコンからテストを実行する
  • Notion Image

    テストが成功すると、緑のチェックマークが表示される。

    Notion Image

    テストが失敗するようにコードを書き換えて再度テストを実行すると、赤いバツマークとエラーメッセージが表示される。

    # tests/test_sample.py 
    
    from src import sample
    
    def test_add():
        assert main.add(1, 2) == 4  # ←テストが失敗するように変更
    Notion Image

    テスト駆動開発を体験する(実践編)

    例として「じゃんけんプログラム」をテスト駆動開発で実装していく。

  • 振る舞いリストを作成する【準備】
  • 【振る舞いリスト】
    ・場に出ている手に対して、どの手が強いのか判定する(対象)
    ・各プレイヤーの勝ち負け引き分けを判定する(対象)

  • 「対象」の振る舞いに対してテストコードを記述して、テストを実行する【レッド】
  • # tests/test_main.py
    
    from src import main
    
    
    def test_get_strong_hand():
        assert main.get_strong_hand(['Goo','Par']) == 'Par'
        assert main.get_strong_hand(['Goo','Choki']) == 'Goo'
        assert main.get_strong_hand(['Choki','Par']) == 'Choki'
        assert main.get_strong_hand(['Goo','Goo']) is None
        assert main.get_strong_hand(['Choki','Choki']) is None
        assert main.get_strong_hand(['Par','Par']) is None
        assert main.get_strong_hand(['Goo','Choki','Par']) is None
        assert main.get_strong_hand(['Goo','Par','Par']) == 'Par'
        assert main.get_strong_hand(['Goo','Choki','Choki']) == 'Goo'
    
    
    def test_judge_win_lose():
        assert main.judge_win_lose('Goo', strong_hand='Goo') == "Win"
        assert main.judge_win_lose('Choki', strong_hand='Choki') == "Win"
        assert main.judge_win_lose('Par', strong_hand='Par') == "Win"
        assert main.judge_win_lose('Par', strong_hand='Choki') == "Lose"
        assert main.judge_win_lose('Par', strong_hand=None) == "Drow"

    テストを実行すると、get_strong_hand関数とjudge_win_lose関数はまだ実装していないのでAttributeErrorがでる。

    AttributeError: module 'src.main' has no attribute 'get_strong_hand'
    AttributeError: module 'src.main' has no attribute 'judge_win_lose'

  • 「対象」の振る舞いを実装して、テストを実行する【グリーン】
  • # src/main.py
    
    # 強い手を返す関数の定義
    def get_strong_hand(hands):
        unique_hands = set(hands)
        if unique_hands == set(['Goo', 'Choki']):
            return 'Goo'
        elif unique_hands == set(['Choki', 'Par']):
            return 'Choki'
        elif unique_hands == set(['Goo', 'Par']):
            return 'Par'
        else:
            return None
        
        
    # 勝敗を判定する関数の定義
    def judge_win_lose(hand, strong_hand):
        if strong_hand is None:
            return 'Drow'
        elif hand == strong_hand:
            return 'Win'
        else:
            return 'Lose'

    ※ 上のコードをそのまま実行すると、すぐにテストを成功させるられるが、実際の実装時は何回もテストに失敗し、エラーメッセージをみながら試行錯誤した後にテストを成功させることになる。


  • 必要に応じて、リファクタリングを実施する(今回は省略)【リファクタリング】

  • コアロジックの実装はできたので、周辺機能として、次の手の生成する関数とその関数を実行する関数を作成する【フィードバック】
  • 【振る舞いリスト】
    ・場に出ている手に対して、どの手が強いのか判定する(済)
    ・各プレイヤーの勝ち負け引き分けを判定する(済)
    ・次の手の生成する関数を作成する(対象)
    ・次の手の生成する関数を実行する関数を作成する(対象)
  • 「対象」の振る舞いに対してテストコードを記述して、テストを実行する【レッド】
  • # tests/test_main.py
    
    from src import main
    
    
    def test_get_strong_hand():
        assert main.get_strong_hand(['Goo','Par']) == 'Par'
        assert main.get_strong_hand(['Goo','Choki']) == 'Goo'
        assert main.get_strong_hand(['Choki','Par']) == 'Choki'
        assert main.get_strong_hand(['Goo','Goo']) is None
        assert main.get_strong_hand(['Choki','Choki']) is None
        assert main.get_strong_hand(['Par','Par']) is None
        assert main.get_strong_hand(['Goo','Choki','Par']) is None
        assert main.get_strong_hand(['Goo','Par','Par']) == 'Par'
        assert main.get_strong_hand(['Goo','Choki','Choki']) == 'Goo'
    
    
    def test_judge_win_lose():
        assert main.judge_win_lose('Goo', strong_hand='Goo') == "Win"
        assert main.judge_win_lose('Choki', strong_hand='Choki') == "Win"
        assert main.judge_win_lose('Par', strong_hand='Par') == "Win"
        assert main.judge_win_lose('Par', strong_hand='Choki') == "Lose"
        assert main.judge_win_lose('Par', strong_hand=None) == "Drow"
    
    
    ### ↓ 以下にテストコードを追加 ###
    def test_constant_hand_generator():
        hand_generator = main.constant_hand_generator('Goo')
        assert next(hand_generator) == 'Goo'
        assert next(hand_generator) == 'Goo'
        assert next(hand_generator) == 'Goo'
    
        hand_generator = main.constant_hand_generator('Choki')
        assert next(hand_generator) == 'Choki'
        assert next(hand_generator) == 'Choki'
        assert next(hand_generator) == 'Choki'
    
        hand_generator = main.constant_hand_generator('Par')
        assert next(hand_generator) == 'Par'
        assert next(hand_generator) == 'Par'
        assert next(hand_generator) == 'Par'
    
    
    def test_get_player_snapshot():
        player = {'name':'Hogehoge', 'hand_generator':main.constant_hand_generator('Par')}
        player_snapshot = main.get_player_snapshot(player)
        assert player_snapshot == {'name':'Hogehoge', 'hand':'Par'}

    テストを実行すると、constant_hand_generator関数はまだ実装していないのでAttributeErrorがでる。

    AttributeError: module 'src.main' has no attribute 'constant_hand_generator'

  • 「対象」の振る舞いを実装して、テストを実行する【グリーン】
  • # src/main.py
    
    # 強い手を返す関数の定義
    def get_strong_hand(hands):
        unique_hands = set(hands)
        if unique_hands == set(['Goo', 'Choki']):
            return 'Goo'
        elif unique_hands == set(['Choki', 'Par']):
            return 'Choki'
        elif unique_hands == set(['Goo', 'Par']):
            return 'Par'
        else:
            return None
        
    
    # 勝敗を判定する関数の定義
    def judge_win_lose(hand, strong_hand):
        if strong_hand is None:
            return 'Drow'
        elif hand == strong_hand:
            return 'Win'
        else:
            return 'Lose'
            
            
    ### ↓ 以下にプロダクトコードを追加 ###
    # 次の手を生成する関数の定義
    def constant_hand_generator(hand):
        while True:
            yield hand
    
    
    # 次の手を生成する関数を実行する関数の定義
    def get_player_snapshot(player):
        player_snapshot = {
            'name': player['name'],
            'hand': next(player['hand_generator'])
        }
        return player_snapshot
    

    ※ 上のコードをそのまま実行すると、すぐにテストを成功させるられるが、実際の実装時は何回もテストに失敗し、エラーメッセージをみながら試行錯誤した後にテストを成功させることになる。


  • 必要に応じて、リファクタリングを実施する(今回は省略)【リファクタリング】

  • 今のままでは毎回同じ手しか出せないので、次の手を「グー→チョキ→パー」の順に生成する関数とランダムに生成する関数を追加する【フィードバック】
  • 【振る舞いリスト】
    ・場に出ている手に対して、どの手が強いのか判定する(済)
    ・各プレイヤーの勝ち負け引き分けを判定する(済)
    ・次の手の生成する関数を作成する(済)
    ・次の手の生成する関数を実行する関数を作成する(済)
    ・次の手を「グー→チョキ→パー」の順に生成する関数を追加する(対象)
    ・次の手をランダムに生成する関数を追加する(対象)

  • 「対象」の振る舞いに対してテストコードを記述して、テストを実行する【レッド】
  • # tests/test_main.py
    
    from src import main
    
    
    def test_get_strong_hand():
        assert main.get_strong_hand(['Goo','Par']) == 'Par'
        assert main.get_strong_hand(['Goo','Choki']) == 'Goo'
        assert main.get_strong_hand(['Choki','Par']) == 'Choki'
        assert main.get_strong_hand(['Goo','Goo']) is None
        assert main.get_strong_hand(['Choki','Choki']) is None
        assert main.get_strong_hand(['Par','Par']) is None
        assert main.get_strong_hand(['Goo','Choki','Par']) is None
        assert main.get_strong_hand(['Goo','Par','Par']) == 'Par'
        assert main.get_strong_hand(['Goo','Choki','Choki']) == 'Goo'
    
    
    def test_judge_win_lose():
        assert main.judge_win_lose('Goo', strong_hand='Goo') == "Win"
        assert main.judge_win_lose('Choki', strong_hand='Choki') == "Win"
        assert main.judge_win_lose('Par', strong_hand='Par') == "Win"
        assert main.judge_win_lose('Par', strong_hand='Choki') == "Lose"
        assert main.judge_win_lose('Par', strong_hand=None) == "Drow"
    
    
    def test_get_constant_hand_generator():
        hand_generator = main.constant_hand_generator('Goo')
        assert next(hand_generator) == 'Goo'
        assert next(hand_generator) == 'Goo'
        assert next(hand_generator) == 'Goo'
    
        hand_generator = main.constant_hand_generator('Choki')
        assert next(hand_generator) == 'Choki'
        assert next(hand_generator) == 'Choki'
        assert next(hand_generator) == 'Choki'
    
        hand_generator = main.constant_hand_generator('Par')
        assert next(hand_generator) == 'Par'
        assert next(hand_generator) == 'Par'
        assert next(hand_generator) == 'Par'
    
    
    def test_get_player_snapshot():
        player = {'name':'Hogehoge', 'hand_generator':main.constant_hand_generator('Par')}
        player_snapshot = main.get_player_snapshot(player)
        assert player_snapshot['name'] == 'Hogehoge'
        assert player_snapshot['hand'] == 'Par'
    
    ### ↓ 以下にテストコードを追加 ###
        player = {'name':'Hogehoge', 'hand_generator':main.turn_hand_generator()}
        player_snapshot = main.get_player_snapshot(player)
        assert player_snapshot['name'] == 'Hogehoge'
        assert player_snapshot['hand'] == 'Goo'
    
    
    def test_get_turn_generator():
        hand_generator = main.turn_hand_generator()
        assert next(hand_generator) == 'Goo'
        assert next(hand_generator) == 'Choki'
        assert next(hand_generator) == 'Par'
        assert next(hand_generator) == 'Goo'
        assert next(hand_generator) == 'Choki'
        assert next(hand_generator) == 'Par'
    
    
    def test_get_randam_generator():
        hand_generator = main.randam_hand_generator()
        assert next(hand_generator) in ['Goo','Choki','Par']
        assert next(hand_generator) in ['Goo','Choki','Par']
        assert next(hand_generator) in ['Goo','Choki','Par']

    テストを実行すると、turn_hand_generator関数とrandam_hand_generator関数はまだ実装していないのでAttributeErrorがでる。

    AttributeError: module 'src.main' has no attribute 'turn_hand_generator'
    AttributeError: module 'src.main' has no attribute 'randam_hand_generator'

  • 「対象」の振る舞いを実装して、テストを実行する【グリーン】
  • # src/main.py
    
    import random
    
    
    # 強い手を返す関数の定義
    def get_strong_hand(hands):
        unique_hands = set(hands)
        if unique_hands == set(['Goo', 'Choki']):
            return 'Goo'
        elif unique_hands == set(['Choki', 'Par']):
            return 'Choki'
        elif unique_hands == set(['Goo', 'Par']):
            return 'Par'
        else:
            return None
        
        
    # 勝敗を判定する関数の定義
    def judge_win_lose(hand, strong_hand):
        if strong_hand is None:
            return 'Drow'
        elif hand == strong_hand:
            return 'Win'
        else:
            return 'Lose'
            
            
    # 次の手を生成する関数の定義
    def constant_hand_generator(hand):
        while True:
            yield hand
    
    
    # 次の手を生成する関数を実行する関数の定義
    def get_player_snapshot(player):
        player_snapshot = {
            'name': player['name'],
            'hand': next(player['hand_generator'])
        }
        return player_snapshot
        
    
    ### ↓ 以下にプロダクトコードを追加 ###
    # 次の手を「グー→チョキ→パー」の順に生成する関数
    def turn_hand_generator():
        count = 0
        turn_hands = ['Goo', 'Choki', 'Par']
        while True:
            yield turn_hands[count % 3]
            count += 1
    
    
    # 次の手をランダムに生成する関数の定義
    def randam_hand_generator():
        while True:
            yield random.choice(['Goo', 'Choki', 'Par'])

    ※ 上のコードをそのまま実行すると、すぐにテストを成功させるられるが、実際の実装時は何回もテストに失敗し、エラーメッセージをみながら試行錯誤した後にテストを成功させることになる。


  • 必要に応じて、リファクタリングを実施する(今回は省略)【リファクタリング】

  • 今のままでは、ユーザーが詳細を意識しないと利用できないので、詳細の処理を内部に隠蔽し、簡単にじゃんけんを実施できる関数を作成する【フィードバック】
  • 【振る舞いリスト】
    ・場に出ている手に対して、どの手が強いのか判定する(済)
    ・各プレイヤーの勝ち負け引き分けを判定する(済)
    ・次の手の生成する関数を作成する(済)
    ・次の手の生成する関数を実行する関数を作成する(済)
    ・次の手を「グー→チョキ→パー」の順に生成する関数を追加する(済)
    ・次の手をランダムに生成する関数を追加する(済)
    ・複数の関数をまとめてじゃんけんを実施する関数を作成する(対象)

  • 「対象」の振る舞いに対してテストコードを記述して、テストを実行する【レッド】
  • # tests/test_main.py
    
    from src import main
    
    
    def test_get_strong_hand():
        assert main.get_strong_hand(['Goo','Par']) == 'Par'
        assert main.get_strong_hand(['Goo','Choki']) == 'Goo'
        assert main.get_strong_hand(['Choki','Par']) == 'Choki'
        assert main.get_strong_hand(['Goo','Goo']) is None
        assert main.get_strong_hand(['Choki','Choki']) is None
        assert main.get_strong_hand(['Par','Par']) is None
        assert main.get_strong_hand(['Goo','Choki','Par']) is None
        assert main.get_strong_hand(['Goo','Par','Par']) == 'Par'
        assert main.get_strong_hand(['Goo','Choki','Choki']) == 'Goo'
    
    
    def test_judge_win_lose():
        assert main.judge_win_lose('Goo', strong_hand='Goo') == "Win"
        assert main.judge_win_lose('Choki', strong_hand='Choki') == "Win"
        assert main.judge_win_lose('Par', strong_hand='Par') == "Win"
        assert main.judge_win_lose('Par', strong_hand='Choki') == "Lose"
        assert main.judge_win_lose('Par', strong_hand=None) == "Drow"
    
    
    def test_get_constant_hand_generator():
        hand_generator = main.constant_hand_generator('Goo')
        assert next(hand_generator) == 'Goo'
        assert next(hand_generator) == 'Goo'
        assert next(hand_generator) == 'Goo'
    
        hand_generator = main.constant_hand_generator('Choki')
        assert next(hand_generator) == 'Choki'
        assert next(hand_generator) == 'Choki'
        assert next(hand_generator) == 'Choki'
    
        hand_generator = main.constant_hand_generator('Par')
        assert next(hand_generator) == 'Par'
        assert next(hand_generator) == 'Par'
        assert next(hand_generator) == 'Par'
    
    
    def test_get_player_snapshot():
        player = {'name':'Hogehoge', 'hand_generator':main.constant_hand_generator('Par')}
        player_snapshot = main.get_player_snapshot(player)
        assert player_snapshot['name'] == 'Hogehoge'
        assert player_snapshot['hand'] == 'Par'
    
    
    def test_get_turn_generator():
        hand_generator = main.turn_hand_generator()
        assert next(hand_generator) == 'Goo'
        assert next(hand_generator) == 'Choki'
        assert next(hand_generator) == 'Par'
        assert next(hand_generator) == 'Goo'
        assert next(hand_generator) == 'Choki'
        assert next(hand_generator) == 'Par'
    
    
    def test_get_randam_generator():
        hand_generator = main.randam_hand_generator()
        assert next(hand_generator) in ['Goo','Choki','Par']
        assert next(hand_generator) in ['Goo','Choki','Par']
        assert next(hand_generator) in ['Goo','Choki','Par']
    
    
    ### ↓ 以下にテストコードを追加 ###
    def test_fight():
        player1 = {'name':'Emi', 'hand_generator':main.constant_hand_generator('Par')}
        player2 = {'name':'Bob', 'hand_generator':main.constant_hand_generator('Par')}
        player3 = {'name':'Ken', 'hand_generator':main.turn_hand_generator()}
        players = [player1, player2, player3]
    
        # 1回戦
        result = main.fight(players)
        assert result == [
            {'name':'Emi', 'hand':'Par', 'judge':'Win'},
            {'name':'Bob', 'hand':'Par', 'judge':'Win'},
            {'name':'Ken', 'hand':'Goo', 'judge':'Lose'},
        ]
    
        # 2回戦
        result = main.fight(players)
        assert result == [
            {'name':'Emi', 'hand':'Par', 'judge':'Lose'},
            {'name':'Bob', 'hand':'Par', 'judge':'Lose'},
            {'name':'Ken', 'hand':'Choki', 'judge':'Win'},
        ]
    
        # 3回戦
        result = main.fight(players)
        assert result == [
            {'name':'Emi', 'hand':'Par', 'judge':'Drow'},
            {'name':'Bob', 'hand':'Par', 'judge':'Drow'},
            {'name':'Ken', 'hand':'Par', 'judge':'Drow'},
        ]
    
        # Bobが手を変更して4回戦
        player2 = {'name':'Bob', 'hand_generator':main.constant_hand_generator('Choki')}
        players = [player1, player2, player3]
        result = main.fight(players)
        assert result == [
            {'name':'Emi', 'hand':'Par', 'judge':'Drow'},
            {'name':'Bob', 'hand':'Choki', 'judge':'Drow'},
            {'name':'Ken', 'hand':'Goo', 'judge':'Drow'},
        ]

    テストを実行すると、fight関数はまだ実装していないのでAttributeErrorがでる。

    AttributeError: module 'src.main' has no attribute 'fight'

  • 「対象」の振る舞いを実装して、テストを実行する【グリーン】
  • # src/main.py
    
    import random
    
    
    # 強い手を返す関数の定義
    def get_strong_hand(hands):
        unique_hands = set(hands)
        if unique_hands == set(['Goo', 'Choki']):
            return 'Goo'
        elif unique_hands == set(['Choki', 'Par']):
            return 'Choki'
        elif unique_hands == set(['Goo', 'Par']):
            return 'Par'
        else:
            return None
        
        
    # 勝敗を判定する関数の定義
    def judge_win_lose(hand, strong_hand):
        if strong_hand is None:
            return 'Drow'
        elif hand == strong_hand:
            return 'Win'
        else:
            return 'Lose'
            
            
    # 次の手を生成する関数の定義
    def constant_hand_generator(hand):
        while True:
            yield hand
    
    
    # 次の手を生成する関数を実行する関数の定義
    def get_player_snapshot(player):
        player_snapshot = {
            'name': player['name'],
            'hand': next(player['hand_generator'])
        }
        return player_snapshot
        
    
    # 次の手を「グー→チョキ→パー」の順に生成する関数
    def turn_hand_generator():
        count = 0
        turn_hands = ['Goo', 'Choki', 'Par']
        while True:
            yield turn_hands[count % 3]
            count += 1
    
    
    # 次の手をランダムに生成する関数の定義
    def randam_hand_generator():
        while True:
            yield random.choice(['Goo', 'Choki', 'Par'])
        
        
    ### ↓ 以下にプロダクトコードを追加 ###  
    # じゃんけんを実施する関数
    def fight(players):
        players_snapshot = [get_player_snapshot(player) for player in players]
        hands = [player_snapshot['hand'] for player_snapshot in players_snapshot]
        strong_hand = get_strong_hand(hands)
        result = [{
            'name': player_snapshot['name'],
            'hand': player_snapshot['hand'],
            'judge': judge_win_lose(player_snapshot['hand'], strong_hand)
        } for player_snapshot in players_snapshot]
        return result
        
        
    if __name__ == "__main__":
        player1 = {'name':'Emi', 'hand_generator':constant_hand_generator('Par')}
        player2 = {'name':'Ken', 'hand_generator':randam_hand_generator()}
        player3 = {'name':'Bob', 'hand_generator':turn_hand_generator()}
        players = [player1, player2, player3]
        result = fight(players)
        print(result)

    ※ 上のコードをそのまま実行すると、すぐにテストを成功させるられるが、実際の実装時は何回もテストに失敗し、エラーメッセージをみながら試行錯誤した後にテストを成功させることになる。


  • 最後に、型を追加してコードの頑健性を上げる【リファクタリング】
  • # src/main.py
    
    import random
    from typing import Optional, Literal, TypedDict, Callable
    from collections.abc import Iterator
    
    
    # 型の定義
    Hand = Literal['Goo', 'Choki', 'Par']
    Judge = Literal['Win', 'Drow', 'Lose']
    
    class Player(TypedDict):
        name: str
        hand_generator: Iterator[Hand]
    
    class PlayerSnapshot(TypedDict):
        name: str
        hand: Hand
    
    class Result(TypedDict):
        name: str
        hand: Hand
        judge: Judge
    
    
    # 強い手を返す関数の定義
    def get_strong_hand(hands: list[Hand]) -> Optional[Hand]:
        unique_hands = set(hands)
        if unique_hands == set(['Goo', 'Choki']):
            return 'Goo'
        elif unique_hands == set(['Choki', 'Par']):
            return 'Choki'
        elif unique_hands == set(['Goo', 'Par']):
            return 'Par'
        else:
            return None
        
        
    # 勝敗を判定する関数の定義
    def judge_win_lose(hand: Hand, strong_hand: Optional[Hand]) -> Judge:
        if strong_hand is None:
            return 'Drow'
        elif hand == strong_hand:
            return 'Win'
        else:
            return 'Lose'
            
            
    # 次の手を生成する関数の定義
    def constant_hand_generator(hand: Hand) -> Iterator[Hand]:
        while True:
            yield hand
        
    
    # 次の手を「グー→チョキ→パー」の順に生成する関数
    def turn_hand_generator() -> Iterator[Hand]:
        count = 0
        turn_hands: list[Hand] = ['Goo', 'Choki', 'Par']
        while True:
            yield turn_hands[count % 3]
            count += 1
    
    
    # 次の手をランダムに生成する関数の定義
    def randam_hand_generator() -> Iterator[Hand]:
        while True:
            yield random.choice(['Goo', 'Choki', 'Par'])
    
    
    # 次の手を生成する関数を実行する関数の定義
    def get_player_snapshot(player: Player) -> PlayerSnapshot:
        player_snapshot: PlayerSnapshot = {
            'name': player['name'],
            'hand': next(player['hand_generator'])
        }
        return player_snapshot
        
        
    # じゃんけんを実施する関数
    def fight(players: list[Player]) -> list[Result]:
        players_snapshot = [get_player_snapshot(player) for player in players]
        hands = [player_snapshot['hand'] for player_snapshot in players_snapshot]
        strong_hand = get_strong_hand(hands)
        results: list[Result] = [{
            'name': player_snapshot['name'],
            'hand': player_snapshot['hand'],
            'judge': judge_win_lose(player_snapshot['hand'], strong_hand)
        } for player_snapshot in players_snapshot]
        return results
        
        
    if __name__ == "__main__":
        player1: Player = {'name':'Emi', 'hand_generator':constant_hand_generator('Par')}
        player2: Player = {'name':'Ken', 'hand_generator':randam_hand_generator()}
        player3: Player = {'name':'Bob', 'hand_generator':turn_hand_generator()}
        players = [player1, player2, player3]
        results = fight(players)
        print(results)

  • リファクタリングが終わったら、テストを実行してバグが入り込んでいないかチェックする。

  • 自動テストから始める

    テスト駆動開発は強力だが、最初は実践するのが難しく感じるかもしれない。テスト駆動開発の中でもっとも重要なのは自動テストである。最初のうちは、必ずしもテストファーストでなくてもかまわないので、自動テストを書くように心がけよう。

    どこまでテストするか

    テストはバグの発見を早めるために実施される。そのため、怪しい部分を集中的にテストして、単純すぎてバグが入り込む余地がないと思えるならテストする必要はないかもしれない。テストによって得られるメリットとテストを書いたり実行したりする時にかかるコストのバランスを考慮して、どこまでテストするか検討する必要がある。

    参考資料



    著者画像

    ゆうき

    2018/04からITエンジニアとして活動、2021/11から独立。主な使用言語はPython, TypeScript, SAS, etc.