0
0

Pythonで〇×ゲームのAIを一から作成する その32 × の勝利の判定に対するテストと test_judge の改良

Last updated at Posted at 2023-11-30

目次と前回の記事

実装の進捗状況と前回までのおさらい

〇×ゲームの仕様と進捗状況

  1. 正方形で区切られた 3 x 3 の 2 次元のゲーム盤上でゲームを行う
  2. ゲーム開始時には、ゲーム盤のすべてのマスは空になっている
  3. 2 人のプレイヤーが遊ぶゲームであり、一人は 〇 を、もう一人は × のマークを受け持つ
  4. 2 人のプレイヤーは、交互に空いている好きなマスに自分のマークを 1 つ置く
  5. 先手は 〇 のプレイヤーである
  6. プレイヤーがマークを置いた結果、縦、横、斜めのいずれかの一直線の 3 マスに同じマークが並んだ場合、そのマークのプレイヤーの勝利とし、ゲームが終了する
  7. すべてのマスが埋まった時にゲームの決着がついていない場合は引き分けとする

仕様の進捗状況は、以下のように表記します。

  • 実装が完了した部分を 背景が灰色の長方形 で記述する
  • 実装の一部が完了した部分を、太字 で記述する

これまでに作成したモジュール

以下のリンクから、これまでに作成したモジュールを見ることができます。

前回までのおさらい

前回の記事では、Marubatsu クラスの judge メソッドに対して、改良条件判断網羅(以後は MC/DC と表記します)でのテストを開始しました。具体的には、〇 の勝利の判定を行う if 文に対するテストケースを作成し、テストを行いました。

今回の記事ではその続きを行います。

改良条件判断網羅(MC/DC)によるテストの続き

前回の記事で、〇 の勝利の判定を行う if 文に対するテストを行ったので、次は、× の勝利の判定 を行う if 文に対するテストを行います。なお、表記が長いので、以降は「× の勝利の判定を行う if 文に対するテストケース」を、× の勝利のテストケース のように表記します。

× が勝利した場合のテストケース

下記は、judge メソッドの中で、× の勝利を判定する if 文です。

        # × の勝利の判定
        if self.board[0][0] == self.board[1][0] == self.board[2][0] == Marubatsu.CROSS or \
        self.board[0][1] == self.board[1][1] == self.board[2][1] == Marubatsu.CROSS or \
        self.board[0][2] == self.board[1][2] == self.board[2][2] == Marubatsu.CROSS or \
        self.board[0][0] == self.board[0][1] == self.board[0][2] == Marubatsu.CROSS or \
        self.board[1][0] == self.board[1][1] == self.board[1][2] == Marubatsu.CROSS or \
        self.board[2][0] == self.board[2][1] == self.board[2][2] == Marubatsu.CROSS or \
        self.board[0][0] == self.board[1][1] == self.board[2][2] == Marubatsu.CROSS or \
        self.board[2][0] == self.board[1][1] == self.board[0][2] == Marubatsu.CROSS:
            winner = Marubatsu.CROSS   

このプログラムは、〇 の勝利を判定 する if 文と 同様 に、条件式は、or 演算子のみ連結 されています。従って、この if 文に対する MC/DC のテストケースは、〇 の勝利のテストケースと 同様 に、下記の性質を満たす ようなテストケースを用意すれば良いことがわかります。

  • or 演算子連結 された の計算結果が すべて False になる テストケース
  • or 演算子連結 された の計算結果のうち、1 つだけTrue になる テストケース

すべてが False になるテストケース

上記のうち、すべてが False になるテストケースは、〇 の勝利のテストケース として用意した、ゲーム盤に一つも マークが配置されていない テストケースを そのまま利用 することができます。従って、すべてが False になるテストケースを 新しく作成 する 必要はありません

1 つだけが True になるテストケース

× の勝利を判定する if 文の条件式の中の、or 演算子で連結された それぞれ式が True になるのは、下図 のマスに × が配置された場合 です。下図の 8 通りいずれの場合 でも、同時2 つ以上 の場所で × が 3 つ並ぶことはない ので、1 つだけTrue になる ことが 保証 されます。従って、下図 のように × が配置 された、8 つ のテストケースを 用意すれば良い ことがわかります。

次に、その 8 つのテストケース に対する judge メソッド期待される返り値 について考える必要があります。ここまでの説明は、前回の記事 の、〇 が勝利した場合同様 なので、この後の説明も同じような説明になると思う人がいるかもしれませんが、ここから の説明は 異なります

× を 3 つ配置 するということは、その前〇 を 3 回配置 することになります。従って、下図のように、〇 が 3 つ並んで配置 するようなテストケースを作成しても、× の勝利を判定する if 文の条件式の 1 つだけTrue になる ことに変わりはありません。

ただし、下記の 〇×ゲームの 仕様 6 で、同じマークが 3 つ並んだ場合 は、ゲームが終了 することが決められています。従って、上記のような 〇 と ×同時に 3 つ並ぶ ような 状況 が起きることは あり得ない ことがわかります。

プレイヤーがマークを置いた結果、縦、横、斜めのいずれかの一直線の 3 マスに同じマークが並んだ場合、そのマークのプレイヤーの勝利とし、ゲームが終了する

実際 の 〇×ゲームで 出現しない ようなゲーム盤の状況を表す テストケース を作成してテストを行っても 意味はない ので、× の勝利のテストケースは、〇 が勝利していない状態作成する必要 があります。

× の勝利のテストケースでは、着手〇 の 3 回× の 3 回 の、計 6 回 になるので、引き分け になることは ありません。従って、引き分けを判定 する if 文の 条件式 の計算結果も 必ず False になります。

下図は、上記の 条件を満たす テストケースに対する judge メソッド処理の流れ を表す フローチャート で、赤い枠線と線 が行われる 処理の流れ を表します。先ほど説明したように、用意するテストケースは、〇 が勝利しない ように作られるので、〇 の勝利を判定 する if 文の条件式の計算結果は 常に False になります。図から、winner に関する 代入処理 は、赤字winner = Marubatsu.CROSS最後に行われる ので、judge メソッドの 期待される返り値Marubatsu.CROSS であることがわかります。

下記は、上記の条件を満たす 8 つの テストケースを記述したプログラムです。なお、前回の記事で記述した、〇 の勝利 のテストケースを 記述しない理由後述 します。

from marubatsu import Marubatsu

testcases = {
    # × の勝利のテストケース
    Marubatsu.CROSS: [
        "A2,A1,B2,B1,A3,C1",
        "A1,A2,B1,B2,A3,C2",
        "A1,A3,B1,B3,A2,C3",
        "B1,A1,B2,A2,C1,A3",
        "A1,B1,A2,B2,C1,B3",
        "A1,C1,A2,C2,B1,C3",
        "A2,A1,A3,B2,B1,C3",
        "A1,C1,B1,B2,A2,A3",
    ],
}

次に、このテストケースに対して test_judge を実行 してテストを行います。まず、実行結果を見て正しいマス にマークが 配置 されていることを 必ず確認 して下さい。

from test import test_judge

test_judge(testcases)

実行結果

Turn o
xxx
oo.
o..

ok
Turn o
oo.
xxx
o..

ok
Turn o
oo.
o..
xxx

ok
Turn o
xoo
xo.
x..

ok
Turn o
oxo
ox.
.x.

ok
Turn o
oox
o.x
..x

ok
Turn o
xo.
ox.
o.x

ok
Turn o
oox
ox.
x..

test_judge error!
mb.judge(): playing
winner:     x

実行結果に 表示される、8 つのテストケースによって着手が行われた ゲーム盤 から、正しいマス にマークが 配置 されていることが 確認 できます。

次に、judge メソッド が、期待される仮り値 を返しているかどうかを 確認 します。下記の 実行結果最後の部分 から、最後のテストケース に対して、mb.judge() の返り値 が、期待される返り値 を表す winner に代入された "x" とは 異なる"playing" になっていることがわかります。

略
Turn o
oox
ox.
x..

test_judge error!
mb.judge(): playing
winner:     x

このエラーメッセージが表示されるのは、mb.judge() の返り値と winner に代入された値の どちらか、または 両方間違っている ことが原因なので、その点について検証します。

このテストケースは、(2, 0)、(1, 1)、(0, 2) のマスに × が並んでいるため、× が勝利 しています。従って、期待される返り値を表す winner"x" である ことは 正しい です。従って、mb.judge() の返り値 が、ゲームが決着していない ことを表す "playing" になっている点が 間違っています

このことから judge メソッド の、× の勝利を判定 する if 文の条件式の中で、(2, 0)、(1, 1)、(0, 2) のマスに × が配置される ことを判定する部分にバグが存在する 可能性が高い ことがわかります。

judge メソッドの その部分の条件式 を調べてみると、下記のプログラムのように、(2, 0)、(1, 1)、(2, 2) という 間違ったマス判定 していることがわかります。

        self.board[2][0] == self.board[1][1] == self.board[2][2] == Marubatsu.CROSS:

従って、この部分を下記のプログラムのように、正しいマスを調べる ように 修正 することでこの バグを修正 することができます。

        self.board[2][0] == self.board[1][1] == self.board[0][2] == Marubatsu.CROSS:
修正箇所
-       self.board[2][0] == self.board[1][1] == self.board[2][2] == Marubatsu.CROSS:
+       self.board[2][0] == self.board[1][1] == self.board[0][2] == Marubatsu.CROSS:

上記以外の部分は修正していないので、修正した judge メソッドの全体をこの記事に記述しませんが(github のほうには記述して実行します)、修正後に下記のプログラムを実行することで、最後のテストケースで エラーメッセージ表示されなくなりjudge メソッドの バグが修正 されたことが 確認 できます。なお、実行結果は、最後の部分のみを記述します。

先程と同じなので省略

Turn o
oox
ox.
x..

ok

これが、judge メソッドの、3 つあるバグの中の 1 つ目のバグ です。このバグのような、データの入力ミス原因 となるバグは よくある事 です。特に、judge メソッドのように、大量のデータを入力 する必要があるプログラムでは、頻発する ので 注意が必要 です。

このように、MC/DC テスト を行うことで、judge メソッドに バグ が存在することを 発見 し、エラーメッセージからその 原因を特定 して 修正 することができました。

test_judge の改良

上記で、× の勝利を判定する if 文のテストケースの作成が終了しましたが、現状の test_judge には いくつかの問題 があるので、次に行く前に 改良 を行うことにします。

× の勝利のテストケースのみを記述した理由

先程、× の勝利 のテストケースを 記述 する際に、前回の記事で作成した 〇 の勝利 のテストケースを 記述しなかった理由 について説明します。

下記は、〇 の勝利 のテストケースを 含めた プログラムです。

testcases = {
    # ゲーム盤に一つもマークが配置されていない場合のテストケース
    Marubatsu.PLAYING: [
        "",
    ],   
    # 〇の勝利のテストケース
    Marubatsu.CIRCLE: [
        "A1,A2,B1,B2,C1",
        "A2,A1,B2,B1,C2",
        "A3,A1,B3,B1,C3",
        "A1,B1,A2,B2,A3",
        "B1,A1,B2,A2,B3",
        "C1,A1,C2,A2,C3",
        "A1,A2,B2,A3,C3",
        "A3,A1,B2,A2,C1",
    ],
    # × の勝利のテストケース
    Marubatsu.CROSS: [
        "A2,A1,B2,B1,A3,C1",
        "A1,A2,B1,B2,A3,C2",
        "A1,A3,B1,B3,A2,C3",
        "B1,A1,B2,A2,C1,A3",
        "A1,B1,A2,B2,C1,B3",
        "A1,C1,A2,C2,B1,C3",
        "A2,A1,A3,B2,B1,C3",
        "A1,C1,B1,B2,A2,A3",
    ],
}

このテストケースに対して test_judgeテストを行う と、下記の実行結果のように、すべてのテストケース に対する ゲーム盤の表示 が行われるため、非常に長い表示 が行われてしまします。

test_judge(testcases)
実行結果(長いので、クリックすると表示するようにしました)
Turn o
...
...
...

ok
Turn x
ooo
xx.
...

ok
Turn x
xx.
ooo
...

ok
Turn x
xx.
...
ooo

ok
Turn x
ox.
ox.
o..

ok
Turn x
xo.
xo.
.o.

ok
Turn x
x.o
x.o
..o

ok
Turn x
o..
xo.
x.o

ok
Turn x
x.o
xo.
o..

ok
Turn o
xxx
oo.
o..

ok
Turn o
oo.
xxx
o..

ok
Turn o
oo.
o..
xxx

ok
Turn o
xoo
xo.
x..

ok
Turn o
oxo
ox.
.x.

ok
Turn o
oox
o.x
..x

ok
Turn o
xo.
ox.
o.x

ok
Turn o
oox
ox.
x..

ok

JupyterLab では、一定以上の行数 の長い 表示 を行うようなプログラムを実行した場合に、表示の一部 のみが表示され、その後下記のような表示 が行われる場合があります。

Output is truncated. View as a scrollable element or open in a text editor. Adjust cell output settings...

最初の文は、出力(output)(表示の事)が 途中切り捨てられている(truncated)という意味です。その後の scrollable element の部分は リンク になっており、クリック することで、スクロール可能 (scrollable)な セル表示が行われる ようになります。

また、その後の text editor のリンクを クリック することで、VSCode の 新しいタブ に、(文字のみを扱う)テキストエディタ として 結果が表示 されるようになります。

プログラムの実行結果を、切り捨てず何行まで表示 するかは、VSCode の 設定で変更 することができます。最後に表示される settings のリンクを クリック することで、その設定を行う画面が VSCode の 新しいタブ に表示されます。変更したい人は、Notebook Output Text Limit の項目が表示する行数を表すので、その部分を修正して下さい。

× の勝利 のテストケースを 新しく記述 して テストを行う 際に、上記 のような 実行結果が表示 された場合の事を 考えてみて下さい。上記の実行結果で表示されたゲーム盤のうち、新しく追加 した × の勝利 のテストケース 以外ゲーム盤の表示 は、既に 前回の記事で マークの配置確認済 なので、表示 する 必要はありません

先程、× の勝利 のテストケース のみを記述 してテストを行った理由は、新しく記述 したテストケースに対して のみ正しいマスマークが配置 されていることを 確認 するためです。

テストケースを 新しく作成 した場合は、その テストケースだけ で、test_judge を実行 して、テストケースによって 正しいマスにマークが配置 されることを 確認 すると良いでしょう。

テストケースの管理方法

上記の説明を読んだ人で、テストケースを追加する際に、全て のテストケースを まとめて記述 するの ではなく、テストケースを 追加するたび に、異なる変数 を用意して、その変数に 追加する テストケースを記述したものを 代入 したほうが良いのではないかと思った人はいないでしょうか?

もちろん、複数の変数 でテストケースを 管理 してテストを行うことは可能ですが、そのような方法は、「変数の管理大変 になる」、「テストを行う際に、一部 のテストケースを使うことを 忘れてしまう 可能性が生じる」などの 欠点 があるため、あまりお勧めしません。

また、一度 テストを行ってjudge メソッドが 期待される処理を行う ことが 確認 されたテストケースは、二度と利用する必要 がないので、複数の変数 でテストケースを 管理しても良い のではないかと 思う人がいるかもしれません が、それは 大きな間違 いです。

これまでの記事で何度も行ってきたように、一度 定義した関数 を、後から修正 することは良くある事です。関数を修正 した場合は、当然ですが、修正した関数が 正しく動作 するかを テストする必要 があります。全てのテストケースを まとめて記述 し、testcases のような 変数に代入 ておくことで、テストの やり直しtest_judge(testcases)呼び出すだけ簡単に実行 できます。

一方、全て のテストケースを まとめて記述 するという方法では、test_judgeテストを行うたび に、すべて のテストケースに対応する ゲーム盤が表示 されてしまうという 問題 があります。ただし、この問題は、test_judge改良 することで 簡単に解決 することができます。

test_judge の改良 その 1(ゲーム盤の表示の有無の選択)

具体的には、test_judge に、ゲーム盤表示するどうか を表す 仮引数を追加 するという方法です。この方法では、仮引数の値True の場合 のみtest_judgeゲーム盤を表示 します。

このような目的の 仮引数の名前 として、ゲーム盤の表示 が、正しいマスにマークが配置されていないという バグを発見 するという、デバッグ のための 表示 であることから debug という名前が 良く使われる1ので、本記事でもこの名前にします。また、test_judgeゲーム盤を表示 する 必要がある のは、テストケース新しく記述 した 場合だけ なので、この仮引数を デフォルト引数 とし、デフォルト値False とします。

下記は、test_judge を修正したプログラムです。修正箇所 は、仮引数 debug の追加 と、10
行目に if 文 を記述し、debugTrue の場合 のみ ゲーム盤を表示 するようにした点です。

 1  from test import excel_to_xy
 2
 3  def test_judge(testcases, debug=False):
 4     for winner, testdata_list in testcases.items():
 5         for testdata in testdata_list:
 6             mb = Marubatsu()
 7             for coord in [] if testdata == "" else testdata.split(","):
 8                 x, y = excel_to_xy(coord)            
 9                 mb.move(x, y)
10             if debug:
11                 print(mb)
12
13              if mb.judge() == winner:
14                  print("ok")
15              else:
16                  print("test_judge error!")
17                  print("mb.judge():", mb.judge())
18                  print("winner:    ", winner)
行番号のないプログラム
from test import excel_to_xy

def test_judge(testcases, debug=False):
    for winner, testdata_list in testcases.items():
        for testdata in testdata_list:
            mb = Marubatsu()
            for coord in [] if testdata == "" else testdata.split(","):
                x, y = excel_to_xy(coord)            
                mb.move(x, y)
            if debug:
                print(mb)

            if mb.judge() == winner:
                print("ok")
            else:
                print("test_judge error!")
                print("mb.judge():", mb.judge())
                print("winner:    ", winner)
修正箇所
from test import excel_to_xy

-def test_judge(testcases):
+def test_judge(testcases, debug=False):
    for winner, testdata_list in testcases.items():
        for testdata in testdata_list:
            mb = Marubatsu()
            for coord in [] if testdata == "" else testdata.split(","):
                x, y = excel_to_xy(coord)            
                mb.move(x, y)
-           print(mb)
+           if debug:
+               print(mb)

            if mb.judge() == winner:
                print("ok")
            else:
                print("test_judge error!")
                print("mb.judge():", mb.judge())
                print("winner:    ", winner)

下記のプログラムのように、2 つ目実引数True を記述 して test_judge を呼び出すことで、ゲーム盤を表示 したテストが行われます。実行結果は先ほどと同じなので省略します。

test_judge(testcases, True)

2 つ目の実引数 が何を意味するかが 分かりにくい と思った方は、下記のように、キーワード引数 を使って、記述すると良いでしょう。本記事でもわかりやすさを重視してこのように記述します。

test_judge(testcases, debug=True)

debugデフォルト値False が設定 された デフォルト引数 なので、下記のように debug に対応する 実引数を省略 することで、ゲーム盤を表示せずテストを行う ことができます。

test_judge(testcases)

実行結果

ok
ok
ok
ok
ok
ok
ok
ok
ok
ok
ok
ok
ok
ok
ok
ok
ok

test_judge の改良 その 2(期待された処理が行われた場合の表示)

先程の実行結果では、judge メソッドが 期待された値を返した ことを表す "ok"大量に表示 されましたが、テストで 知りたいこと は、以前の記事 で説明したように、期待された処理行われなかった テストケースが あるかどうかを調べること なので、期待された処理が行われたことを表す "ok" を表示しても あまり意味はありません。また、テストでは、一般的に ほとんど のテストケースに対して 期待された処理が行われる ので、そのことを 画面に表示 すると、上記のように画面が メッセージで埋め尽くされる ことになり、肝心エラーメッセージ埋もれて わからなくなってしまう 可能性が高く なります。

そこで、下記のプログラムのように、test_judge期待された処理行われた場合 は、メッセージ表示しない ように 修正 することにします。具体的には、12 行目の print("ok")ブロックの中何の処理も行わない ことを表す pass に修正 します。

 1  def test_judge(testcases, debug=False):
 2     for winner, testdata_list in testcases.items():
 3         for testdata in testdata_list:
 4             mb = Marubatsu()
 5             for coord in [] if testdata == "" else testdata.split(","):
 6                 x, y = excel_to_xy(coord)            
 7                 mb.move(x, y)
 8             if debug:
 9                 print(mb)
10
11              if mb.judge() == winner:
12                  pass
13              else:
14                  print("test_judge error!")
15                  print("mb.judge():", mb.judge())
16                  print("winner:    ", winner)
行番号のないプログラム
def test_judge(testcases, debug=False):
    for winner, testdata_list in testcases.items():
        for testdata in testdata_list:
            mb = Marubatsu()
            for coord in [] if testdata == "" else testdata.split(","):
                x, y = excel_to_xy(coord)            
                mb.move(x, y)
            if debug:
                print(mb)

            if mb.judge() == winner:
                pass
            else:
                print("test_judge error!")
                print("mb.judge():", mb.judge())
                print("winner:    ", winner)
修正箇所
def test_judge(testcases, debug=False):
    for winner, testdata_list in testcases.items():
        for testdata in testdata_list:
            mb = Marubatsu()
            for coord in [] if testdata == "" else testdata.split(","):
                x, y = excel_to_xy(coord)            
                mb.move(x, y)
            if debug:
                print(mb)

            if mb.judge() == winner:
-               print("ok")
+               pass
            else:
                print("test_judge error!")
                print("mb.judge():", mb.judge())
                print("winner:    ", winner)

修正後に、test_judge を実行すると、下記のように、何も表示されなくなります。

test_judge(testcases)

実行結果

ブロックの中で何の処理も行わない場合は、必ず pass を記述 する 必要 があり、pass省略 すると、下記のプログラムのように エラーが発生 します。

if True: # この if 文のブロックが記述されていない
a = 1

実行結果

  Cell In[13], line 2
    a = 1
    ^
IndentationError: expected an indented block after 'if' statement on line 1

上記のエラーメッセージは、以下のような意味を持ちます。

  • IndentationError
    インデント(indent)に関するエラー
  • expected an indented block
    インデントされた(indented)ブロック(block)が期待されている(expected)

pass がわかりにくいと思った人は、下記のように、judge メソッドの返り値と、期待される返り値が 等しくない 場合にエラーメッセージを表示するようにすると良いでしょう。

            if mb.judge() != winner:
                print("test_judge error!")
                print("mb.judge():", mb.judge())
                print("winner:    ", winner)
修正箇所
-           if mb.judge() == winner:
-               pass
-           else:
+           if mb.judge() != winner:
                print("test_judge error!")
                print("mb.judge():", mb.judge())
                print("winner:    ", winner)

test_judge の改良 その 3(途中経過の表示)

先程の改良によって、すべてのテストケースで judge メソッドが期待された値を返す場合は、何も 表示が行われなく なりますが、何かの処理を行った結果、何も表示されない と、うまくいったかどうかがわからないので、不安になる 人が多いのではないかと思います。

そのような場合は、途中経過 を表す メッセージを表示 するのが一般的です。

どのようなメッセージが わかりやすい かについては、人によって違う ので唯一の正解はありません。本記事では、下記のようなメッセージを表示することにしますが、もっと良いメッセージを思いついた人は、自分なりに自由にアレンジして下さい。

  • テストを開始 したことを表示する(2 行目)
  • 期待される返り値変わった時 に、その値 を表示する(4 行目)
  • judge メソッドの返り値が 期待される返り値の場合 に、そのことを 短く表示 する(14 行目)
  • テストを終了 したことを表示する(20 行目)

下記は、上記のメッセージを表示するように修正したプログラムです。上記の()の中は、それぞれのメッセージを表示するプログラムを記述した行数を表します。

 1  def test_judge(testcases, debug=False):
 2      print("Start")
 3      for winner, testdata_list in testcases.items():
 4          print("test winner =", winner) 
 5          for testdata in testdata_list:
 6              mb = Marubatsu()
 7              for coord in [] if testdata == "" else testdata.split(","):
 8                  x, y = excel_to_xy(coord)            
 9                  mb.move(x, y)
10              if debug:
11                  print(mb)
12
13              if mb.judge() == winner:
14                  print("o", end="")
15              else:
16                  print("test_judge error!")
17                  print("mb.judge():", mb.judge())
18                  print("winner:    ", winner)
19          print()
20      print("Finished")
行番号のないプログラム
def test_judge(testcases, debug=False):
    print("Start")
    for winner, testdata_list in testcases.items():
        print("test winner =", winner) 
        for testdata in testdata_list:
            mb = Marubatsu()
            for coord in [] if testdata == "" else testdata.split(","):
                x, y = excel_to_xy(coord)            
                mb.move(x, y)
            if debug:
                print(mb)

            if mb.judge() == winner:
                print("o", end="")
            else:
                print("test_judge error!")
                print("mb.judge():", mb.judge())
                print("winner:    ", winner)
        print()
    print("Finished")
修正箇所
def test_judge(testcases, debug=False):
    print("Start")
    for winner, testdata_list in testcases.items():
+       print("test winner =", winner) 
        for testdata in testdata_list:
            mb = Marubatsu()
            for coord in [] if testdata == "" else testdata.split(","):
                x, y = excel_to_xy(coord)            
                mb.move(x, y)
            if debug:
                print(mb)

            if mb.judge() == winner:
-               pass
+               print("o", end="")
            else:
                print("test_judge error!")
                print("mb.judge():", mb.judge())
                print("winner:    ", winner)
+       print()
+   print("Finished")

テストケースの数 があまり 多くない 場合は、上記の 14 行目のように、judge メソッドの返り値が期待される返り値であった場合に、"o" のような 短いメッセージを表示 することで、テストが行われていることを 実感できるようにする ことができます。ただし、テストケースの数 が、例えば 10000 のように 非常に多い 場合は、このようなメッセージは 表示しないほうが良い でしょう。なお、print"o"改行せずに表示 するために、end="" を実引数に記述しています。

19 行目の print() は、3 行目の for 文の 次の繰り返し行う前 に、改行を行う ために記述しています。これを 記述しない と、14 行目で表示した "o" の直後 に 4 行目の print のメッセージが表示 されてしまいます。具体例はこの後のノートを参照して下さい。

修正後に、test_judge を実行すると、下記のようなメッセージが表示されるようになります。test winner = の次の行の "o" の数 は、期待される返り値対応するテストケースの数 です。

test_judge(testcases)

実行結果

Start
test winner = playing
o
test winner = o
oooooooo
test winner = x
oooooooo
Finished

19 行目の print() を記述しないと、実行結果は以下のように、test winner の前 などに "o" が表示 されるようになります。興味がある方は、実際に 19 行目を削除して test_judge を実行してみて下さい。

Start
test winner = playing
otest winner = o
ooooooootest winner = x
ooooooooFinished

test_judge の改良 その 3 の問題点

先程実行した、test_judge(testcases) では、テストケースの中に、下記の if 文条件式False になる ようなものが 存在しない ため、else のブロック一度も実行されていません でした。そのため、改良後test_judge で、下記の if 文の条件式が False になった場合 にどのような メッセージが表示 されるかを 確認 しておいたほうが良いでしょう。

これは、すべての文1 度は実行する という、命令網羅(C0) のテストを行うということです。

            if mb.judge() == winner:
                print("o", end="")
            else:
                print("test_judge error!")
                print("mb.judge():", mb.judge())
                print("winner:    ", winner)

この if 文の条件式が False になるのは、judge メソッドが 期待される返り値返さない 場合です。確認の方法 として、judge メソッドバグのある状態に戻す という方法が考えられますが、それよりも、テストケースのデータ のほうを 間違ったデータに修正 したほうが 簡単 です。

そこで、テストケースを下記のように、わざと間違ったデータ修正 します。具体的には、下記のプログラムの、コメントが書かれている行のテストデータの最後に記述されていた "B3" を削除 することで、〇 が勝利していない テストデータに修正しています。なお、このデータは正しいデータが代入された testcases区別ができる ように、testcases_error という変数に代入します。

testcases_error = {
    # ゲーム盤に一つもマークが配置されていない場合のテストケース
    Marubatsu.PLAYING: [
        "",
    ],   
    # 〇の勝利のテストケース
    Marubatsu.CIRCLE: [
        "A1,A2,B1,B2,C1",
        "A2,A1,B2,B1,C2",
        "A3,A1,B3,B1,C3",
        "A1,B1,A2,B2,A3",
        "B1,A1,B2,A2",      # このデータの最後の B3 を削除しました
        "C1,A1,C2,A2,C3",
        "A1,A2,B2,A3,C3",
        "A3,A1,B2,A2,C1",
    ],
    # × の勝利のテストケース
    Marubatsu.CROSS: [
        "A2,A1,B2,B1,A3,C1",
        "A1,A2,B1,B2,A3,C2",
        "A1,A3,B1,B3,A2,C3",
        "B1,A1,B2,A2,C1,A3",
        "A1,B1,A2,B2,C1,B3",
        "A1,C1,A2,C2,B1,C3",
        "A2,A1,A3,B2,B1,C3",
        "A1,C1,B1,B2,A2,A3",
    ],
}

修正後に、test_judge を実行すると、下記のようなメッセージが表示されます。このエラーメッセージが わかりにくい 主な 理由 が 2 つあるので、その理由について少し考えてみて下さい。

test_judge(testcases_error)

実行結果

Start
test winner = playing
o
test winner = o
ooootest_judge error!
mb.judge(): playing
winner:     o
ooo
test winner = x
oooooooo
Finished

メッセージがわかりにくい 主な理由 は以下の 2 点です。

  • ooootest_judge error! の行で、ooootest_judge error! の間で 改行されていない
  • エラーメッセージの前に、ゲーム盤表示されていない ので、どのような状況 でエラーが発生したかが わからない

一つ目は、test_judge error!直前で改行 を表示することで解決できます(16 行目)。

二つ目は、test_judge error!後でゲーム盤を表示 することで解決できます(18 行目)。

下記は、そのように修正したプログラムです。上記の()の中が修正箇所を表します。

 1  def test_judge(testcases, debug=False):
 2      print("Start")
 3      for winner, testdata_list in testcases.items():
 4          print("test winner =", winner) 
 5          for testdata in testdata_list:
 6              mb = Marubatsu()
 7              for coord in [] if testdata == "" else testdata.split(","):
 8                  x, y = excel_to_xy(coord)            
 9                  mb.move(x, y)
10              if debug:
11                  print(mb)
12
13              if mb.judge() == winner:
14                  print("o", end="")
15              else:
16                  print()
17                  print("test_judge error!")
18                  print(mb)
19                  print("mb.judge():", mb.judge())
20                  print("winner:    ", winner)
21          print()
22      print("Finished")
行番号のないプログラム
def test_judge(testcases, debug=False):
    print("Start")
    for winner, testdata_list in testcases.items():
        print("test winner =", winner) 
        for testdata in testdata_list:
            mb = Marubatsu()
            for coord in [] if testdata == "" else testdata.split(","):
                x, y = excel_to_xy(coord)            
                mb.move(x, y)
            if debug:
                print(mb)

            if mb.judge() == winner:
                print("o", end="")
            else:
                print()
                print("test_judge error!")
                print(mb)
                print("mb.judge():", mb.judge())
                print("winner:    ", winner)
        print()
    print("Finished")
修正箇所
def test_judge(testcases, debug=False):
    print("Start")
    for winner, testdata_list in testcases.items():
        print("test winner =", winner) 
        for testdata in testdata_list:
            mb = Marubatsu()
            for coord in [] if testdata == "" else testdata.split(","):
                x, y = excel_to_xy(coord)            
                mb.move(x, y)
            if debug:
                print(mb)

            if mb.judge() == winner:
                print("o", end="")
            else:
+               print()
                print("test_judge error!")
+               print(mb)
                print("mb.judge():", mb.judge())
                print("winner:    ", winner)
        print()
    print("Finished")

修正後に、test_judge を実行すると、下記のようなメッセージが表示されます。

test_judge(testcases_error)

実行結果

Start
test winner = playing
o
test winner = o
oooo
test_judge error!
Turn o
xo.
xo.
...

mb.judge(): playing
winner:     o
ooo
test winner = x
oooooooo
Finished

修正後のメッセージは、修正前よりわかりやすく なっていますが、エラーメッセージそうでないメッセージ区別がつきづらい 点がまだ わかりづらい のではないかと思います。

このような場合は、エラーメッセージ前後改行 したり、下記のプログラムの 17、22 行目のように、エラーメッセージの 境目明確になるような表示 を行うという工夫が考えられます。

 1  def test_judge(testcases, debug=False):
 2      print("Start")
 3      for winner, testdata_list in testcases.items():
 4          print("test winner =", winner) 
 5          for testdata in testdata_list:
 6              mb = Marubatsu()
 7              for coord in [] if testdata == "" else testdata.split(","):
 8                  x, y = excel_to_xy(coord)            
 9                  mb.move(x, y)
10              if debug:
11                  print(mb)
12
13              if mb.judge() == winner:
14                  print("o", end="")
15              else:
16                  print()
17                  print("====================")
18                  print("test_judge error!")
19                  print(mb)
20                  print("mb.judge():", mb.judge())
21                  print("winner:    ", winner)
22                  print("====================")
23          print()
24      print("Finished")
行番号のないプログラム
def test_judge(testcases, debug=False):
    print("Start")
    for winner, testdata_list in testcases.items():
        print("test winner =", winner) 
        for testdata in testdata_list:
            mb = Marubatsu()
            for coord in [] if testdata == "" else testdata.split(","):
                x, y = excel_to_xy(coord)            
                mb.move(x, y)
            if debug:
                print(mb)

            if mb.judge() == winner:
                print("o", end="")
            else:
                print()
                print("====================")
                print("test_judge error!")
                print(mb)
                print("mb.judge():", mb.judge())
                print("winner:    ", winner)
                print("====================")
        print()
    print("Finished")
修正箇所
def test_judge(testcases, debug=False):
    print("Start")
    for winner, testdata_list in testcases.items():
        print("test winner =", winner) 
        for testdata in testdata_list:
            mb = Marubatsu()
            for coord in [] if testdata == "" else testdata.split(","):
                x, y = excel_to_xy(coord)            
                mb.move(x, y)
            if debug:
                print(mb)

            if mb.judge() == winner:
                print("o", end="")
            else:
                print()
+               print("====================")
                print("test_judge error!")
                print(mb)
                print("mb.judge():", mb.judge())
                print("winner:    ", winner)
+               print("====================")
        print()
    print("Finished")

修正後に、test_judge を実行すると、下記のようなメッセージが表示されます。

test_judge(testcases_error)

実行結果

Start
test winner = playing
o
test winner = o
oooo
====================
test_judge error!
Turn o
xo.
xo.
...

mb.judge(): playing
winner:     o
====================
ooo
test winner = x
oooooooo
Finished

ずいぶんとわかりやすくなったと思いますので、本記事ではこれを採用することにします。

上記で満足できない方は、自分がわかりやすいと思えるようになるまで、自由に修正して下さい。例えば、期待される返り値が変わった時に表示する、test winner = o の前で改行するなどの工夫が考えられるでしょう。

上記の 16 行目のプログラムは改行を表示するプログラムですが、改行を表す \n という エスケープシーケンス を使うことで、下記の 2 行をまとめる ことが出来ます。

16                  print()
17                  print("====================")

具体的には下記のように記述します。

print("\n====================")
修正箇所
-print()
-print("====================")
+print("\n====================")

今回の記事のまとめ

今回の記事では、× の勝利 を判定する if 文に対する MC/DC のテストを行い、test_judge のバグ の一つを 発見 し、修正 しました。また、test_judge を改良 しました。

大変申し訳ありませんが、test_judge の改良が思いのほか長くなったので今回の記事はここまでにしたいと思います。次回で、テストに関する記事を終了したいと思います。

本記事で入力したプログラム

以下のリンクから、本記事で入力して実行した JupyterLab のファイルを見ることができます。

以下のリンクは、今回の記事で更新した marubatsu.py です。

以下のリンクは、今回の記事で更新した test.py です。

次回の記事

  1. 他にも、詳細という意味を表す verbose という名前が使われるようです

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0