目次と前回の記事
実装の進捗状況と前回までのおさらい
〇×ゲームの仕様と進捗状況
正方形で区切られた 3 x 3 の 2 次元のゲーム盤上でゲームを行う
ゲーム開始時には、ゲーム盤のすべてのマスは空になっている
2 人のプレイヤーが遊ぶゲームであり、一人は 〇 を、もう一人は × のマークを受け持つ
2 人のプレイヤーは、交互に空いている好きなマスに自分のマークを 1 つ置く
先手は 〇 のプレイヤーである
- プレイヤーがマークを置いた結果、縦、横、斜めのいずれかの一直線の 3 マスに同じマークが並んだ場合、そのマークのプレイヤーの勝利とし、ゲームが終了する
- すべてのマスが埋まった時にゲームの決着がついていない場合は引き分けとする
仕様の進捗状況は、以下のように表記します。
- 実装が完了した部分を
背景が灰色の長方形
で記述する - 実装の一部が完了した部分を、太字 で記述する
これまでに作成したモジュール
以下のリンクから、これまでに作成したモジュールを見ることができます。
前回までのおさらい
前回の記事では、× の勝利を判定する 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 演算子 で True
と False
を 反転 した式は、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 c
が False
になります。これは、a
、b
、c
が すべて False
である 場合だけ なので、その場合は、not a
、not b
、not c
は すべて True
になります。従って、(not a) and (not b) and (not c)
も True
になります。
not(a or b or c)
が False
になる 場合 は、a or b or c
が True
になります。この場合は、a
、b
、c
の いずれか 1 つ以上 が True
になるので、not a
、not b
、not 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 演算子 で 連結 する式は a
、b
、c
の 3 つ ですが、式の数 が いくつであっても この 法則は成り立ちます。
図で説明すると以下のようになります。
下図の黄色い部分が、a or b or c
、外側の水色の部分が not(a or b or c)
を表します。
下図のオレンジ色の部分がそれぞれ not a
、not b
、not 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()
が 期待された返り値を返した際 に、debug
が True
の場合は、"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 文の条件式に対して True
と False
になるような 組み合わせを網羅 するテストケースでテストを行うという、簡易的な 経路組み合わせ網羅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
メソッドの 期待される返り値 を考えます。
- この場合は、期待される返り値は明らかに
Marubatsu.PLAYING
です - この場合は、期待される返り値は明らかに
Marubatsu.DRAW
です - この場合は、期待される返り値は明らかに
Marubatsu.CROSS
です - この場合は、勝利の判定 と、引き分けの判定 が 共に
True
になります。一見する と、このような状況は あり得ない ように 思えるかもしれません が、引き分けの判定 を行う 条件式 が、すべてのマスが埋まっている ことを 判定 していることを考えると、最後の 9 手目 で マークを配置 した際に × が勝利 した場合は、この条件が満たされる ことがわかります。このことから、3 つ目の if 文 が「引き分けを判定」しているの ではなく、「すべてのマスが埋まっているかどうか」を 判定 していることがわかります。このような、似ている が 実際には違う ことを判定するという 勘違い は、プログラムを実装 する際に よく発生し、そのことが 原因 で バグが発生 することが よくあります。実際に、この勘違い が、judge
メソッドの 3 つ目のバグの原因 になっています。なお、〇×ゲームの性質 から、9 手目 は、必ず 〇 の手番 になるので、9 手目で × が勝利 することは あり得ません。従って、4 の条件を満たす テストケースは 存在しません - この場合は、期待される返り値は明らかに
Marubatsu.CIRCLE
です - この場合は、上記の 4 で説明 したように、最後の 9 手目 でマークを 配置 した際に 〇が勝利する場合 です。従って、期待される返り値は
Marubatsu.CIRCLE
になります - 〇×ゲームの性質 上、〇 の勝利 と × の勝利 が 同時に起きる ことは ありません。従って、この条件を満たすテストケースは 存在しません
- 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 つ 並べるのではなく、elif や else を使って、一つの if 文 で 条件分岐を記述 する方法が 簡単 です。
下記はそのように修正したプログラムです。修正した箇所は以下の通りです。なお、条件式が長い ので、それぞれの条件式の部分は、「〇 の勝利判定を行う条件式」のように 言葉で表記 します。
- 2 つ目 と 3 つ目 の if を elif に 修正 した
- 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
のブロック内 で、winner
に Marubatsu.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 です。
次回の記事