0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Pythonで〇×ゲームのAIを一から作成する その33 引き分けの判定に対するテストと組み合わせ網羅テスト

Last updated at Posted at 2023-12-03

目次と前回の記事

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

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

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

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

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

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

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

前回までのおさらい

前回の記事では、× の勝利を判定する if 文の MC/DC のテストを行い、test_judge メソッドの改良を行いました。今回の記事では、残りのテスト行います。

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

次は、残りの 引き分けを判定 する if 文の MC/DCテスト を行います。

引き分けの場合のテストケース

下記は、judge メソッドの中で、引き分けの判定 を行う if 文 です。

        # 引き分けの判定
        if not(self.board[0][0] == Marubatsu.EMPTY or \
            self.board[1][0] == Marubatsu.EMPTY or \
            self.board[2][0] == Marubatsu.EMPTY or \
            self.board[0][1] == Marubatsu.EMPTY or \
            self.board[1][1] == Marubatsu.EMPTY or \
            self.board[1][1] == Marubatsu.EMPTY or \
            self.board[0][2] == Marubatsu.EMPTY or \
            self.board[1][2] == Marubatsu.EMPTY or \
            self.board[2][2] == Marubatsu.EMPTY):
            winner = Marubatsu.DRAW 

他の 2 つ の if 文と 異なり、この if 文の 条件式 には not 演算子 が記述されています。この条件式のように、or 演算子のみ連結 された式に対して、not 演算子TrueFalse反転 した式は、and 演算子のみ連結 された式に 変換 することができます。

ド・モルガンの法則

具体的には、下記の 1 行目の式 と、2 行目の式 の計算結果は、常に同じ になります。このような法則の事を「ド・モルガンの法則」と呼びます。

not(a or b or c)
(not a) and (not b) and (not c)

分かりにくいかもしれませんが、このことを 言葉で説明 すると以下のようになります。

not(a or b or c)True になる 場合 は、a or b or cFalse になります。これは、abcすべて False である 場合だけ なので、その場合は、not anot bnot cすべて True になります。従って、(not a) and (not b) and (not c)True になります。

not(a or b or c)False になる 場合 は、a or b or cTrue になります。この場合は、abcいずれか 1 つ以上True になるので、not anot bnot cいずれか 1 つ以上必ず False になります。従って、(not a) and (not b) and (not c)False になります。

上記の事から、not(a or b or c)(not a) and (not b) and (not c) の計算結果は 常に等しい ことになります。なお、上記では、or 演算子連結 する式は abc3 つ ですが、式の数いくつであっても この 法則は成り立ちます

図で説明すると以下のようになります。

下図の黄色い部分が、a or b or c、外側の水色の部分が not(a or b or c) を表します。

下図のオレンジ色の部分がそれぞれ not anot bnot c を表します。(not a) and (not b) and (not c) は、下の 3 つの図の すべてオレンジ色 になっている部分なので、それは上図の水色の部分の not(a or b or c)等しい ことがわかります。

参考までに、ド・モルガンの法則の法則の wikipedia へのリンクを下記に記します。

not(self.board[0][0] == Marubatsu.EMPTY) は、self.board[0][0] != Marubatsu.EMPTY同じ意味 を持つので、引き分けを判定する if 文の 条件式 は、ド・モルガンの法則利用 することで、下記のプログラムのように 書き換える ことができます。

        # 引き分けの判定
        if self.board[0][0] != Marubatsu.EMPTY and \
           self.board[1][0] != Marubatsu.EMPTY and \
           self.board[2][0] != Marubatsu.EMPTY and \
           self.board[0][1] != Marubatsu.EMPTY and \
           self.board[1][1] != Marubatsu.EMPTY and \
           self.board[1][1] != Marubatsu.EMPTY and \
           self.board[0][2] != Marubatsu.EMPTY and \
           self.board[1][2] != Marubatsu.EMPTY and \
           self.board[2][2] != Marubatsu.EMPTY:
            winner = Marubatsu.DRAW 

以前の記事 で説明したように、and 演算子のみ連結 された 条件式 に対する MC/DCテストケース として、下記の性質を満たす ようなテストケースが必要になります。

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

なお、judge メソッドの引き分けを判定する if 文の条件式を上記のように 書き換えた理由 は、MC/DC のテスト を行うための テストケース簡単に求める ことができるようにするためです。

従って、この条件式を、実際 に上記のように 修正 する 必要はありません。修正前と、修正後の条件式は、いずれも 同じ計算 を行うので、分かりやすいと思った ほうを 採用 して下さい。本記事では、修正せずに、元の条件式を採用することにします。

すべて True になる テストケース

修正後条件式 で、すべてが True になるのは、すべて のマスが 空でない 場合です。従って、すべてのマス に何らかの マークが配置 された テストケースを用意 すれば良いことがわかります。

すべてのマスマークが配置 されたテストケースは、かなりの種類 がありますが、MC/DC のテスト では、そのような 条件を満たす テストケースを 1 つだけ用意 すれば良いので、命令網羅(C0) のテストを行った際に 作成 した、下図のテストケースを用意することにします。

上図以外のテストケースを用意したい人は、8 手目まで〇 か × が勝利 すると、そこで ゲームが終了 してしまうため、8 手目まで決着がつかないよう に 9 つの 着手を行う 必要がある点に注意して下さい。

上記のテストケースに対する judge メソッド期待される返り値 は、〇 も × も 3 つ 並んでいない ので、下図の フローチャート から、Marubatsu.DRAW である事がわかります。

下記は、上記のテストケースを記述したプログラムです。

from marubatsu import Marubatsu

testcases = {
    # 引き分けの場合のテストケース
    Marubatsu.DRAW: [
        "A1,A2,B1,B2,C2,C1,A3,B3,C3",
    ],  
}

上記のテストケースに対して、実引数に debug=True を記述 して test_judge を実行します。

from test import test_judge

test_judge(testcases, debug=True)

実行結果

Start
test winner = draw
Turn x
oox
xxo
oxo

o
Finished

実行結果の ゲーム盤の表示 から、テストケースによって 正しいマスにマークが配置 されることが 確認 できます。また、エラーメッセージ表示されない ので、このテストケースに対して、mb.judge()期待される返り値を返す ことが 確認 できます。

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

修正後引き分けを判定 する if 文の 条件式 の中の 1 つだけFalse になる テストケースでは、9 つのマスのうち、1 つのマスだけが空 になるので、全部で 9 つ のテストケースが 必要 になることがわかります。MC/DC のテスト では、そのような 条件を満たす 場合は どのようなテストケース でも 構わない ので、先程と同様に、7 手目までゲームが終了しない ように気を付けて、下図 のテストケースを 用意 することにします。

上図は、いずれも 決着がついていない 状態なので、judge メソッドの 期待される返り値 は、いずれも Marubatsu.PLAYING になります。

決着がついている 状態のテストケースを 用意 しても かまいません が、その場合は、期待される返り値 が何になるかを 考えて テストケースを 記述 する必要があります。

下記は、上図のテストケースを記述したプログラムです。

testcases = {
    # 1 つだけマークが配置されていない場合のテストケース
    Marubatsu.PLAYING: [
        "C3,A2,B1,B2,C2,C1,A3,B3",
        "A1,A2,C3,B2,C2,C1,A3,B3",
        "A1,A2,B1,B2,C2,C3,A3,B3",
        "A1,C3,B1,B2,C2,C1,A3,B3",
        "A1,A2,B1,C3,C2,C1,A3,B3",
        "A1,A2,B1,B2,C3,C1,A3,B3",
        "A1,A2,B1,B2,C2,C1,C3,B3",
        "A1,A2,B1,B2,A3,C1,C2,C3",
        "A1,A2,B1,B2,C2,C1,A3,B3",
    ],  
}

上記のテストケースに対して、実引数に debug=True を記述して test_judge を実行します。

test_judge(testcases, debug=True)

実行結果

Start
test winner = playing
Turn o
.ox
xxo
oxo

oTurn o
o.x
xxo
oxo

oTurn o
oo.
xxo
oxx

oTurn o
oox
.xo
oxx

oTurn o
oox
x.o
oxx

oTurn o
oox
xx.
oxo


====================
test_judge error!
Turn o
oox
xx.
oxo

mb.judge(): draw
winner:     playing
====================
Turn o
oox
xxo
.xo

oTurn o
oox
xxo
o.x

oTurn o
oox
xxo
ox.

o
Finished

実行結果の ゲーム盤の表示 から、テストケースによって 正しいマスにマークが配置 されることが 確認 できます1。一方、(2, 1) のマスマークを配置しない テストケースで、エラーメッセージが表示 されます。エラーメッセージのうち、winner"playing" なのは 正しい ので、mb.judge()返り値"draw" になっている点が おかしいこと がわかります。

そのことを念頭に置いて judge メソッドの 引き分けを判定 する if 文の 条件式調べてみる と、(2, 1) のマス であること 判定 する下記の 7 行目の式が self.board[1][1] == Marubatsu.EMPTY のように、(1, 1) のマスを判定 するようになっていることがわかります。

 1        # 引き分けの判定
 2        if not(self.board[0][0] == Marubatsu.EMPTY or \
 3            self.board[1][0] == Marubatsu.EMPTY or \
 4            self.board[2][0] == Marubatsu.EMPTY or \
 5            self.board[0][1] == Marubatsu.EMPTY or \
 6            self.board[1][1] == Marubatsu.EMPTY or \
 7            self.board[1][1] == Marubatsu.EMPTY or \ # この行が間違っている
 8            self.board[0][2] == Marubatsu.EMPTY or \
 9            self.board[1][2] == Marubatsu.EMPTY or \
10            self.board[2][2] == Marubatsu.EMPTY):
11            winner = Marubatsu.DRAW

従って、その部分を、下記のプログラムのように 修正すれば良い ことがわかります。

        # 引き分けの判定
        if not(self.board[0][0] == Marubatsu.EMPTY or \
            self.board[1][0] == Marubatsu.EMPTY or \
            self.board[2][0] == Marubatsu.EMPTY or \
            self.board[0][1] == Marubatsu.EMPTY or \
            self.board[1][1] == Marubatsu.EMPTY or \
            self.board[2][1] == Marubatsu.EMPTY or \ # [2][1] に修正する
            self.board[0][2] == Marubatsu.EMPTY or \
            self.board[1][2] == Marubatsu.EMPTY or \
            self.board[2][2] == Marubatsu.EMPTY):
            winner = Marubatsu.DRAW
修正箇所
        # 引き分けの判定
        if not(self.board[0][0] == Marubatsu.EMPTY or \
            self.board[1][0] == Marubatsu.EMPTY or \
            self.board[2][0] == Marubatsu.EMPTY or \
            self.board[0][1] == Marubatsu.EMPTY or \
            self.board[1][1] == Marubatsu.EMPTY or \
-           self.board[1][1] == Marubatsu.EMPTY or \
+           self.board[2][1] == Marubatsu.EMPTY or \
            self.board[0][2] == Marubatsu.EMPTY or \
            self.board[1][2] == Marubatsu.EMPTY or \
            self.board[2][2] == Marubatsu.EMPTY):
            winner = Marubatsu.DRAW

長くなるので省略しますが、judge メソッドを上記のように 修正後 に、下記のプログラムを実行することで、修正後の judge メソッドで 期待された返り値が返されること確認 できます。

なお、テストケースによってゲーム盤に 正しくマスが配置されるかどうか は、先程 確認済 なので、test_judge の実引数に debug=True記述 する 必要はありません

test_judge(testcases)

実行結果

Start
test winner = playing
ooooooooo
Finished

これが、judge メソッドの、3 つあるバグの中の 2 つ目のバグ で、このバグも 1 つ目のバグと 同様 に、データの入力ミス原因 となるバグです。

これで、judge メソッドの 2 つ目バグ発見 して 修正 することができました。

test_judge の修正

気づいていない人が多いかもしれませんが、先程の test_judge(testcases, debug=True)実行結果表示 には、少し おかしな所 があります。それが何かを考えてみて下さい。

ヒント:おかしな点は、下記の中にあります。

Start
test winner = playing
Turn o
.ox
xxo
oxo

oTurn o
略

よく見ると、上記の「略」のすぐ上の行に、oTurn o という、おかしな表示が行われています。

この、Turn直前"o" は、judge メソッドが 期待された返り値を返した 時に、print("o", end="") によって 表示 されたものです。debug=True の場合は、"o" の後次の テストケースの ゲーム盤を表示 するので、"o" の後改行を行う 必要があります。また、その場合は、"o" のように 短く表示 する 必要はない ので、元の "ok" を表示 したほうが わかりやすい でしょう。下記のプログラムはそのように修正したプログラムです。

修正箇所は 14 ~ 17 行目で、md.judge()期待された返り値を返した際 に、debugTrue の場合は、"ok" を、そうでなければ"o"改行せず表示 するように修正しています。

 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                  if debug:
15                      print("ok")
16                  else:
17                      print("o", end="")
18              else:
19                  print()
20                  print("====================")
21                  print("test_judge error!")
22                  print(mb)
23                  print("mb.judge():", mb.judge())
24                  print("winner:    ", winner)
25                  print("====================")
26          print()
27      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:
                if debug:
                    print("ok")
                else:
                    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="")
+               if debug:
+                   print("ok")
+               else:
+                   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(testcases, debug=True) を実行することで、下記の実行結果のように、おかしな部分の 表示が修正 されたことが確認できます。

test_judge(testcases, debug=True)

実行結果

Start
test winner = playing
Turn o
.ox
xxo
oxo

ok
Turn o
略

MC/DC のテストをまとめて行う

MC/DC のテスト必要 なテストケースが すべて揃った ので、すべてのテストケースまとめてテストを行う ことにします。下記は、すべてのテストケースを記述したプログラムです。

testcases = {
    # 決着がついていない場合のテストケース
    Marubatsu.PLAYING: [
        # ゲーム盤に一つもマークが配置されていない場合のテストケース
        "",
        # 一つだけマークが配置されていない場合のテストケース
        "C3,A2,B1,B2,C2,C1,A3,B3",
        "A1,A2,C3,B2,C2,C1,A3,B3",
        "A1,A2,B1,B2,C2,C3,A3,B3",
        "A1,C3,B1,B2,C2,C1,A3,B3",
        "A1,A2,B1,C3,C2,C1,A3,B3",
        "A1,A2,B1,B2,C3,C1,A3,B3",
        "A1,A2,B1,B2,C2,C1,C3,B3",
        "A1,A2,B1,B2,A3,C1,C2,C3",
        "A1,A2,B1,B2,C2,C1,A3,B3",
    ],   
    # 〇の勝利のテストケース
    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",
    ],
    # 引き分けの場合のテストケース
    Marubatsu.DRAW: [
        "A1,A2,B1,B2,C2,C1,A3,B3,C3",
    ], 
}

このテストケースに対して、test_judge を実行することで MC/DC のテスト を行うことができます。実行結果から、すべてのテストケース期待される返り値が返される ことが 確認 できます。

test_judge(testcases)

実行結果

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

MC/DC テスト を行うことで、judge メソッドの 3 つあるバグのうちの 2 つを発見 し、修正 することができました。残念ながら、MC/DC テストでは 発見できないバグ1 つ残っています

残りのバグは、次の経路組み合わせ網羅によって発見することができます。

経路組み合わせ網羅によるテスト

経路組み合わせ網羅 は、可能な限り、複数の 条件分岐すべての経路組み合わせ を網羅します。ただし、すべての経路を網羅すると、組み合わせ爆発 が非常に 起きやすい ため、多くの場合で経路組み合わせ網羅によるテストを行うことは 現実的ではありません

例えば、judge メソッドの 経路組み合わせ網羅必要 なテストケースの 数を計算 します。

  • 1 つ目の if 文の条件式は、8 つの式 が or 演算子で連結されているので、経路は $2^8 =$ 256 通り
  • 2 つ目の if 文の条件式も同様なので、経路は 256 通り
  • 3 つ目の if 文の条件式は、9 つの式 が and 演算子で連結されているので、経路は $2^9 =$ 512 通り
  • 従って、すべての経路の組み合わせ は、$256 * 256 * 512 =$ 33554432(約3000万)通り

judge メソッドの場合、通ることが不可能経路がある2ため、実際には上記で計算した数のテストケースがすべて必要になることはありませんが、かなり多く のテストケースが 必要 になるため、経路組み合わせ網羅 でテストを行うことは 現実的ではありません

簡易的な組み合わせ網羅

上記は 厳密な 経路組み合わせ網羅の説明ですが、すべて の if 文の条件式に対して TrueFalse になるような 組み合わせを網羅 するテストケースでテストを行うという、簡易的な 経路組み合わせ網羅3を考えることができます。この場合は、厳密な場合と比較して 組み合わせ爆発おきづらい ので、実際に judge メソッドに対してこのテストを行うことができます。

分かりにくいと思いますので、judge メソッドを具体例として説明します。

judge メソッドには、3 つの if 文 があり、その 条件文計算結果すべての組み合わせは $2 * 2 * 2 =$ 8 通り になります。従って、簡易的な経路組み合わせ網羅では、下記の条件を満たす、8 つ のテストケースを用意すれば良いことがわかります。

下記の はその すべての組み合わせ を表したものです。以降は、それぞれを、表の 左の列 に記述した 番号で表す ことにします。

〇の勝利の判定 ×の勝利の判定 引き分けの判定
1 False False False
2 False False True
3 False True False
4 False True True
5 True False False
6 True False True
7 True True False
8 True True True

次に、各条件 を満たすテストケースに対する、judge メソッドの 期待される返り値 を考えます。

  1. この場合は、期待される返り値は明らかに Marubatsu.PLAYING です
  2. この場合は、期待される返り値は明らかに Marubatsu.DRAW です
  3. この場合は、期待される返り値は明らかに Marubatsu.CROSS です
  4. この場合は、勝利の判定 と、引き分けの判定共に True になります。一見する と、このような状況は あり得ない ように 思えるかもしれません が、引き分けの判定 を行う 条件式 が、すべてのマスが埋まっている ことを 判定 していることを考えると、最後の 9 手目マークを配置 した際に × が勝利 した場合は、この条件が満たされる ことがわかります。このことから、3 つ目の if 文 が「引き分けを判定」しているの ではなく、「すべてのマスが埋まっているかどうか」を 判定 していることがわかります。このような、似ている実際には違う ことを判定するという 勘違い は、プログラムを実装 する際に よく発生し、そのことが 原因バグが発生 することが よくあります。実際に、この勘違い が、judge メソッドの 3 つ目のバグの原因 になっています。なお、〇×ゲームの性質 から、9 手目 は、必ず 〇 の手番 になるので、9 手目で × が勝利 することは あり得ません。従って、4 の条件を満たす テストケースは 存在しません
  5. この場合は、期待される返り値は明らかに Marubatsu.CIRCLE です
  6. この場合は、上記の 4 で説明 したように、最後の 9 手目 でマークを 配置 した際に 〇が勝利する場合 です。従って、期待される返り値は Marubatsu.CIRCLE になります
  7. 〇×ゲームの性質 上、〇 の勝利× の勝利同時に起きる ことは ありません。従って、この条件を満たすテストケースは 存在しません
  8. 7 と同様の理由で、この条件を満たすテストケースは 存在しません

下記は、上記の考察結果を表にしたものです。上記の考察から、3 つ目の if 文 で行う 処理の意味間違っていた ことが分かったので、「引分の判定」を「全てのマスが埋まっているか」に 修正 しました。なお、× は、そのようなテストケースが 存在しない ことを表します。

〇の勝利の判定 ×の勝利の判定 全てのマスが埋まっているか 期待される返り値
1 False False False Marubatsu.PLAYING
2 False False True Marubatsu.DRAW
3 False True False Marubatsu.CROSS
4 False True True ×
5 True False False Marubatsu.CIRCLE
6 True False True Marubatsu.CIRCLE
7 True True False ×
8 True True True ×

テストの実施

簡易的な経路組み合わせ網羅 によるテストでは、上記の 5 種類 のテストケースを 用意 すれば良いことがわかります。上記の中で、1、2、3、5 のテストケースは、MC/DC で作成 したものを 流用 することができます。そのため、新しく作る 必要があるテストケースは、6 の一つだけ です。

本記事では、6 のテストケースとして、下図のようなものを用意することにします。9 手目〇 が勝利 する必要があるため、9 手目の着手(0, 2) にする 必要 がある点に注意して下さい。

下記は、簡易的な経路組み合わせ網羅の 5 個のテストケースを記述したプログラムです。

testcases = {
    Marubatsu.PLAYING: [
        "", # 1 のテストケース
    ],
    Marubatsu.CIRCLE: [
        "A1,A2,B1,B2,C1",             # 5 のテストケース
        "A1,B1,A2,B2,B3,C1,C3,C2,A3", # 6 のテストケース
    ],
    Marubatsu.CROSS: [
        "A2,A1,B2,B1,A3,C1", # 3 のテストケース
    ],
    Marubatsu.DRAW: [
        "A1,A2,B1,B2,C2,C1,A3,B3,C3", # 2 のテストケース
    ], 
}

上記のテストケースに対して、実引数に debug=True を記述して test_judge を実行します。

test_judge(testcases, debug=True)

実行結果

Start
test winner = playing
Turn o
...
...
...

ok

test winner = o
Turn x
ooo
xx.
...

ok
Turn x
oxx
oxx
ooo


====================
test_judge error!
Turn x
oxx
oxx
ooo

mb.judge(): draw
winner:     o
====================

test winner = x
Turn o
xxx
oo.
o..

ok

test winner = draw
Turn x
oox
xxo
oxo

ok

Finished

実行結果の ゲーム盤の表示 から、テストケースによって 正しいマスマークが配置 されることが 確認 できます。一方、6 のテストケース に対して、judge メソッドが、期待される "o" ではなく"draw" を返す という エラーメッセージが表示 されます。

エラーの原因の検証

下図は、6 のテストケースに対して judge メソッドを実行した際の フローチャート です。

図からわかるように、〇 の勝利 の if 文の 条件式が True になった結果、図の 紫色の文字 で表示された winner = Marubatsu.CIRCLE実行 されますが、その後全てのマスが埋まっている ことを判定する if 文の 条件式True になるので、赤色の文字 で表示された winner = Marubatsu.DRAW がその後で実行され、winner の値が Marubatsu.DRAW上書き されてしまいます。

これが、judge メソッドの 3 つ目のバグの原因 です。

バグの修正方法 その 1

このバグを修正する方法はいくつかありますが、〇 の勝利判定 された時点で、〇 の勝利 であると 確定する 場合は、独立した if 文 を 3 つ 並べるのではなくelifelse を使って、一つの if 文条件分岐を記述 する方法が 簡単 です。

下記はそのように修正したプログラムです。修正した箇所は以下の通りです。なお、条件式が長い ので、それぞれの条件式の部分は、「〇 の勝利判定を行う条件式」のように 言葉で表記 します。

  • 2 つ目3 つ目ifelif修正 した
  • 2 つ目3 つ目if 文の前 にあった 空行を削除 した

元のプログラムでは、間に空行記述する ことで、3 つの if 文異なる if 文であることが 明確になる ようにしていましたが、修正後1 つの if 文 になったので、間の空行を削除 しました。

    def judge(self):      
        # 判定を行う前に、決着がついていないことにしておく
        winner = Marubatsu.PLAYING
        # 〇 の勝利の判定
        if 〇の勝利判定を行う条件式:
            winner = Marubatsu.CIRCLE        
        # × の勝利の判定
        elif × の勝利判定を行う条件式:
            winner = Marubatsu.CROSS     
        # すべてのマスが埋まっていない事の判定
        elif すべてのマスが埋まっていない事を判定する条件式:
            winner = Marubatsu.DRAW

        # winner を返り値として返す
        return winner   
修正箇所
    def judge(self):      
        # 判定を行う前に、決着がついていないことにしておく
        winner = Marubatsu.PLAYING
        # 〇 の勝利の判定
        if 〇の勝利判定を行う条件式:
            winner = Marubatsu.CIRCLE     
        # × の勝利の判定
-
-       if × の勝利判定を行う条件式:
+       elif × の勝利判定を行う条件式:
            winner = Marubatsu.CROSS     
        # すべてのマスが埋まっていない事の判定
-
-       if すべてのマスが埋まっていない事を判定する条件式:
+       elif すべてのマスが埋まっていない事を判定する条件式:
            winner = Marubatsu.DRAW

        # winner を返り値として返す
        return winner   

修正後judge メソッドの フローチャート は以下のようになります。

下記のプログラムによって、すべてのテストケース に対して judge メソッドが 期待される処理を返す ようになったことが 確認 できます。

test_judge(testcases, debug=True)

実行結果

Start
test winner = playing
o
test winner = o
oo
test winner = x
o
test winner = draw
o
Finished

バグの修正方法その 2

他の修正方法として、ゲームの決着がついていない状態 では、if 文の 3 つの条件式すべてが False になることから、下記のプログラムのように、else のブロック内 で、winnerMarubatsu.PLAYING代入 するように修正する方法があります。

    def judge(self):      
        # 〇 の勝利の判定
        if 〇の勝利判定を行う条件式:
            winner = Marubatsu.CIRCLE
        # × の勝利の判定
        elif × の勝利判定を行う条件式:
            winner = Marubatsu.CROSS     
        # すべてのマスが埋まっていない事の判定
        elif すべてのマスが埋まっていない事を判定する条件式:
            winner = Marubatsu.DRAW
        # 上記のどれでもなければ決着がついていない
        else:
            winner = Marubatsu.PLAYING   

        # winner を返り値として返す
        return winner   

下記のプログラムを実行することによって、この修正方法で正しくテストが行えることが確認できます。なお、実行結果は先ほどと同様なので省略します。

test_judge(testcases)

修正方法その 1 と その 2 の違い

修正方法その 1 と、その 2 のプログラムは、全く同じ処理 を行うように 見えるかも しれませんが、実際 には、以下のように、微妙に 行われる 処理が異なります

  • 修正方法その 1 のプログラムでは、if 文を実行する前に、必ず winner = Marubatsu.PLAYING実行 される
  • 修正方法その 2 のプログラムでは、if 文のすべての条件式が False場合のみ winner = Marubatsu.PLAYING実行される

judge メソッドの 場合 は、この違いは 全く問題にならない ので、わかりやすいと思ったほうを採用して構いませんが、この違い がプログラムの 処理に影響を及ぼす場合 があります。

例えば、下記のように、judge メソッドを修正 する場合の事を考えてみて下さい。

  • judge メソッドを 実行 し、ゲームが続行中 であることが 判定された場合 に、print("playing") を実行して "playing" という文字列を 画面に表示 する

修正方法その 1 を元に、下記のプログラムのように、4 行目に print("playing") を記述してしまうと、この 4 行目 は、if 文を実行する前に 必ず実行 されるので、judge メソッドを 実行 すると 必ず "playing" が表示 されてしまいまうという バグが発生 します。

1    def judge(self):      
2        # 判定を行う前に、決着がついていないことにしておく
3        winner = Marubatsu.PLAYING
4        print("playing")
5        # 〇 の勝利の判定
6        if 〇の勝利判定を行う条件式:
7            winner = Marubatsu.CIRCLE
8        

一方、修正方法その 2 を元に、下記のプログラムのように、8 行目に print("playing") を記述した場合は、この 8 行目 は、if 文の すべての条件式が False場合のみ実行 されるので、正しい処理が行われます。

1    def judge(self):      
2        # 〇 の勝利の判定
3        if 〇の勝利判定を行う条件式:
4        
5        # 上記のどれでもなければ決着がついていない
6        else:
7            winner = Marubatsu.PLAYING   
8            print("playing")
9        

このように、処理の流れ正しく理解せず にプログラムを記述すると、思わぬバグが発生 してしまうことがあるので、処理の流れを意識 してプログラムを記述することを こころがけて下さい

バグの修正方法その 3

if 文の後 で何も 処理を行わない のであれば、if 文のそれぞれの ブロックの中 で、return 文 を記述することもできます。これは、return 文を実行 すると、関数の残りの処理を行うことなく、関数呼び出しの処理 をその時点で 即座に終了する という性質を利用しています。

この場合は、下記のプログラムのように、ローカル変数 winner不要 になります。下記のようなプログラムは 実際に良く記述される ので、本記事でもこのプログラムを採用することにします

    def judge(self):      
        # 〇 の勝利の判定
        if 〇の勝利判定を行う条件式:
            return Marubatsu.CIRCLE        
        # × の勝利の判定
        elif × の勝利判定を行う条件式:
            return Marubatsu.CROSS     
        # すべてのマスが埋まっていない事の判定
        elif すべてのマスが埋まっていない事を判定する条件式:
            return Marubatsu.DRAW
        # 上記のどれでもなければ決着がついていない
        else:
            return Marubatsu.PLAYING   

下記のプログラムを実行することによって、この修正方法で正しくテストが行えることが確認できます。なお、実行結果は先ほどと同様なので省略します。

test_judge(testcases)

if 文に関する注意点

今回記事の例からわかるように、条件分岐 で、複数の条件式記述 する際に、「複数の if 文を記述」する場合と、「elif を使って 一つの if 文で記述」する場合は、異なる処理 が行われる点に 注意 して下さい。そこを 間違える と修正前の judge メソッドのような バグの原因 になります。

また、複数の条件式記述 する際に、可能 であれば 一つの if 文で記述したほうが良い でしょう。その 理由 は、下記のように、経路の組み合わせが減る からです。

  • 修正前のフローチャートでは、3 つの if 文が独立 していたので、処理の流れ組み合わせ が、$2 ^ 3$ = 8 通り ある
  • 修正後のフローチャートでは、1 つの if 文の中 に、3 つの条件式 があるので、処理の流れの組み合わせは $3 + 1$ = 4 通り ある

上記からわかるように、複数の if 文で記述 する場合は、組み合わせの数 が、乗算で計算 されるため、if 文の数が多くなればなるほど、急激に増えていく ことになります。一方、1 つの if 文で記述 する場合は、組み合わせの数が 加算で計算 されるため、急激に増えることは ありません

MC/DC と簡易的な組み合わせ網羅のテストの併用

簡易的な組み合わせ網羅 のテストでは、それぞれの if 文 に対して、分岐網羅(C1) のテスト しか行っていない ことになります。従って、if 文の条件式に and 演算子or 演算子記述 されている場合は、MC/DC のテストと 併用 したほうが良いでしょう。

実際に、先程の 簡易的な組み合わせ網羅のテスト で用意した 5 つのテストケースでは、MC/DC で発見できたjudge メソッドの 1 つ目と 2 つ目のバグ発見できません

そこで、本記事では最後に、下記のような、MC/DC簡易的な組み合わせ網羅 のそれぞれの条件を満たすテストケースを 集めたテストケース でテストを行うことにします。

testcases = {
    # 決着がついていない場合のテストケース
    Marubatsu.PLAYING: [
        # ゲーム盤に一つもマークが配置されていない場合のテストケース
        "",
        # 一つだけマークが配置されていない場合のテストケース
        "C3,A2,B1,B2,C2,C1,A3,B3",
        "A1,A2,C3,B2,C2,C1,A3,B3",
        "A1,A2,B1,B2,C2,C3,A3,B3",
        "A1,C3,B1,B2,C2,C1,A3,B3",
        "A1,A2,B1,C3,C2,C1,A3,B3",
        "A1,A2,B1,B2,C3,C1,A3,B3",
        "A1,A2,B1,B2,C2,C1,C3,B3",
        "A1,A2,B1,B2,A3,C1,C2,C3",
        "A1,A2,B1,B2,C2,C1,A3,B3",
    ],   
    # 〇の勝利のテストケース
    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",
        # 簡易的な組み合わせ網羅の 6 のテストケース
        "A1,B1,A2,B2,B3,C1,C3,C2,A3", 
    ],
    # × の勝利のテストケース
    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",
    ],
    # 引き分けの場合のテストケース
    Marubatsu.DRAW: [
        "A1,A2,B1,B2,C2,C1,A3,B3,C3",
    ], 
}

下記のプログラムによって、MC/DC と簡易的な組み合わせ網羅の 両方のすべて のテストケースに対して judge メソッドが 期待される処理を返す ことが 確認 できます。

test_judge(testcases)

実行結果

Start
test winner = playing
oooooooooo
test winner = o
ooooooooo
test winner = x
oooooooo
test winner = draw
o
Finished

MC/DC と簡易的な組み合わせ網羅をまとめて行う方法

上記では、MC/DC のテストと、簡易的な組み合わせ網羅のテストを 別々に 行い、その 2 つのテストケースを 合わせて テストを行う方法を紹介しましたが、MC/DC のテスト を行う際に、工夫する ことで 簡易的な組み合わせ網羅 のテストを まとめて(並行して)行う ことができます。

今回の記事の最初のほうで、引き分けの場合の、MC/DC の 「すべてが True になる テストケース」の所で、以下のような説明を行いました。

「すべてのマスにマークが配置されたテストケースは、かなりの種類がありますが、MC/DC のテストでは、そのような 条件を満たす テストケースを 1 つだけ用意 すれば良い」

この部分を、「すべての経路 に対し、そのような 条件を満たす テストケースを 1 つずつ用意 する」のように修正することで、MC/DC のテストと、簡易的な組み合わせ網羅のテストを並行して行うことができるようになります。

具体的には、今回の記事では上記の場合に、「9 つのマスが埋まっている、引き分けの状態」のテストケース のみを用意 してテストを行いましたが、それに加えて、「9 つのマスが埋まっている、〇 の処理の状態」のテストケースを用意してテストを行います。

長くなるので、この方法でテストの具体例は省略しますが、興味がある方は試してみて下さい。

今後の記事での関数のテストの方針について

大変長くなってしまいましたが、以上で judge メソッドのテストは終了です。

個人で作成 するようなプログラムでは、すべての関数 に対して、このようなテストを 行う必要全くありません が、ある程度以上複雑 な関数を実装した場合は、分岐網羅(C1)レベル のテストは 行ったほうが良い のではないかと思います。どのレベル のテストを 行うか については、実装した 関数の複雑 さや、テストに費やすことが出来る時間 などから、自分で判断 して下さい。

なお、本記事では、毎回 テストを行うための関数を作成 するようなテストを行うと、記事が際限なく長く なってしまうので、基本的には 簡単なテストで済ます ことにしようと考えています。

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

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

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

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

次回の記事

  1. 一度で正しくテストケースを記述することは難しいかもしれません。実際に、筆者は、実行結果を見た結果、間違えていることがわかったので、一度修正を行いました

  2. 例えば、〇×ゲームの性質上、〇 の勝利と 引き分けが同時に満たされるようなテストケースは存在しません

  3. 厳密な経路組み合わせ網羅と、簡易的な経路組み合わせ網羅を表現する用語を探したのですが見つかりませんでした。知っている方がいれば教えて頂ければ助かります

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?