目次と前回の記事
実装の進捗状況と前回までのおさらい
〇×ゲームの仕様と進捗状況
正方形で区切られた 3 x 3 の 2 次元のゲーム盤上でゲームを行う
ゲーム開始時には、ゲーム盤のすべてのマスは空になっている
2 人のプレイヤーが遊ぶゲームであり、一人は 〇 を、もう一人は × のマークを受け持つ
2 人のプレイヤーは、交互に空いている好きなマスに自分のマークを 1 つ置く
先手は 〇 のプレイヤーである
- プレイヤーがマークを置いた結果、縦、横、斜めのいずれかの一直線の 3 マスに同じマークが並んだ場合、そのマークのプレイヤーの勝利とし、ゲームが終了する
- すべてのマスが埋まった時にゲームの決着がついていない場合は引き分けとする
仕様の進捗状況は、以下のように表記します。
- 実装が完了した部分を
背景が灰色の長方形
で記述する - 実装の一部が完了した部分を、太字 で記述する
これまでに作成したモジュール
以下のリンクから、これまでに作成したモジュールを見ることができます。
test.py
が扱うテストケースは、Excel 座標を ","
で区切って連結したデータです。
前回までのおさらい
前回の記事までで、テストケースを記述するためのデータ構造やアルゴリズムについて説明しました。今回の記事では、中断していたテストの作業を再開し、バグを修正します。
テストに関するおさらい
長い間テストの作業を中断していたので、テストに関するおさらいをします。
以前の記事 で、勝敗判定 を行う下記の Marubatsu
クラスの judge
メソッド を実装した際に、judge
メソッドに バグが存在する ことを説明しました。
def judge(self):
# 判定を行う前に、決着がついていないことにしておく
winner = None
# 〇 の勝利の判定
if self.board[0][0] == self.board[1][0] == self.board[2][0] == Marubatsu.CIRCLE or \
self.board[0][1] == self.board[1][1] == self.board[2][1] == Marubatsu.CIRCLE or \
self.board[0][2] == self.board[1][2] == self.board[2][2] == Marubatsu.CIRCLE or \
self.board[0][0] == self.board[0][1] == self.board[0][2] == Marubatsu.CIRCLE or \
self.board[1][0] == self.board[1][1] == self.board[1][2] == Marubatsu.CIRCLE or \
self.board[2][0] == self.board[2][1] == self.board[2][2] == Marubatsu.CIRCLE or \
self.board[0][0] == self.board[1][1] == self.board[2][2] == Marubatsu.CIRCLE or \
self.board[2][0] == self.board[1][1] == self.board[0][2] == Marubatsu.CIRCLE:
winner = Marubatsu.CIRCLE
# × の勝利の判定
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[2][2] == Marubatsu.CROSS:
winner = Marubatsu.CROSS
# 引き分けの判定
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
# winner を返り値として返す
return winner
実装したプログラムが 正しく動作 することを 確認する作業 のことを テスト と呼び、judge
メソッドのような、複雑な処理 を行う関数を 実装 した場合は、テストを行う ことが 重要 です。
以前の記事では、テストの手法 の一つである、制御フローテスト について説明し、制御フローテストには、下記の表のような、いくつかの 種類がある 事を説明しました。
次に、以前の記事 で、下記の表の中の 命令網羅(C0)、分岐網羅(C1)のテストを行いましたが、これらのテストでは test_judge
の中にあるバグを 発見 することは できませんでした。
名称 | 略称 | テストケースの数 | 精度 |
---|---|---|---|
命令網羅 | C0、SC | 最小 | 最小 |
分岐網羅 | C1、DC | 小 | 小 |
条件網羅 | C2、CC | 小~中 | 小 |
判定条件/条件網羅 | CDC | 小~中 | 中 |
複数条件網羅 | MCC | 大 | 大 |
改良条件判断網羅 | MC/DC | 中 | 大 |
経路組み合わせ網羅 | なし | 最大 | 最大 |
以前の記事で説明したように、本記事では条件網羅(C2)、判定条件/条件網羅(CDC)、複数条件網羅(MCC)は 行いません。今回の記事では残りのテストのうち、改良条件判断網羅 (MC/DC)のテストを開始します。
改良条件判断網羅(MC/DC)によるテスト
改良条件判断網羅(以後は MC/DC と表記します) では、以下のようなテストを行います。
条件分岐の 条件式の中 に、and または or の 論理演算子 が記述されている場合、それらで連結された それぞれの式の値 が、全体の条件式 の計算結果に 影響を及ぼす(因果関係がある)かどうかを 考慮 したテストを行う。
MC/DC のテストを行うために必要な テストケースを求める方法 は、以前の記事で説明したように、一般的 にはそれほど 簡単ではありません が、条件式 が or 演算子のみ で 連結 されている場合は、以前の記事で説明した下記の方法で 簡単に求める ことができます。
-
or 演算子 で 連結 された 式 の計算結果が すべて
False
になる テストケース -
or 演算子 で 連結 された 式 の計算結果のうち、1 つだけ が
True
になる テストケース
judge
メソッドには 〇 の勝利、× の勝利、引き分け を 判定 する 3 つの if 文 が記述されており、前者の 2 つ は 条件式 が or 演算子のみ で 連結 されているので、それら の if 文に対する テストケース を 簡単に求める ことが出来ます。引き分けの場合に関しては次回の記事で説明します。
〇 が勝利した場合のテストケース
まず、〇が勝利した場合 の下記の if 文に対する テストケースを求める ことにします。
if self.board[0][0] == self.board[1][0] == self.board[2][0] == Marubatsu.CIRCLE or \
self.board[0][1] == self.board[1][1] == self.board[2][1] == Marubatsu.CIRCLE or \
self.board[0][2] == self.board[1][2] == self.board[2][2] == Marubatsu.CIRCLE or \
self.board[0][0] == self.board[0][1] == self.board[0][2] == Marubatsu.CIRCLE or \
self.board[1][0] == self.board[1][1] == self.board[1][2] == Marubatsu.CIRCLE or \
self.board[2][0] == self.board[2][1] == self.board[2][2] == Marubatsu.CIRCLE or \
self.board[0][0] == self.board[1][1] == self.board[2][2] == Marubatsu.CIRCLE or \
self.board[2][0] == self.board[1][1] == self.board[0][2] == Marubatsu.CIRCLE:
winner = Marubatsu.CIRCLE
上記の if 文の条件式は、8 つの式 が or 演算子 で 連結 されており、それぞれの式 は、〇 が下図のように、直線上に 3 つ並んでいる かどうかを 判定 しています。
すべてが False
になるテストケース
この条件式の中で、「or 演算子 で 連結 された 式 の計算結果が すべて False
」になるテストケースとして 最も簡単 なものは、ゲーム盤に一つも マークが配置されていない 状態です。以前の記事で説明したように、テストケース は、「テストデータ と、期待される処理 を 組み合わせたもの」なので、「ゲーム盤に一つもマークが配置されていない状態」で judge
メソッドを実行した際に 期待される処理 である、judge
メソッド の 返り値 が何になるかを 考える必要 があります。
ゲーム盤に一つもマークが配置されていない場合は、〇の勝利、×の勝利、引き分け の いずれの場合でもない ので、judge
メソッドの 3 つの if 文 の 条件式 の計算結果は すべて False
になります。従って、judge
メソッドの最初でローカル変数 winner
に代入 された None
という値 は、変更されることは無い ので、judge
メソッドの 返り値 は None
になる ことが 期待されます。
下図の 赤い線 と 枠線 が、ゲーム盤に マークが配置されていない場合 に judge
メソッドを実行した場合の 処理の流れ を表しており、図から、関数の返り値 が None
になる ことがわかります。
本記事では、着手 を行う 複数の座標 を表す データ構造 として、以前の記事 で説明した Excel 座標 を ","
で区切って 連結 するデータ構造を採用します。下記は、そのデータ構造でゲーム盤に一つもマークが配置されていない場合の テストケースを記述 したプログラムです。このテストケースでは、着手を行わない ので、着手を表すデータは 空の文字列 である ""
になります。
from marubatsu import Marubatsu
testcases = [
# ゲーム盤に一つもマークが配置されていない場合のテストケース
[
"",
None,
],
]
テストの確認
このテストケースに対して test_judge
でテストを行うと、以下のような エラーが発生 します。
from test import test_judge
test_judge(testcases)
実行結果
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
c:\Users\ys\ai\marubatsu\031\marubatsu.ipynb セル 2 line 3
1 from test import test_judge
----> 3 test_judge(testcases)
File c:\Users\ys\ai\marubatsu\031\test.py:25, in test_judge(testcases)
23 mb = Marubatsu()
24 for coord in testdata.split(","):
---> 25 x, y = excel_to_xy(coord)
26 mb.move(x, y)
27 print(mb)
File c:\Users\ys\ai\marubatsu\031\test.py:48, in excel_to_xy(coord)
34 def excel_to_xy(coord: str) -> tuple(int):
35 """ Excel 座標を xy 座標に変換する.
36
37 "A1" のような、文字列で表現される Excel 座標を、
(...)
46 x 座標と y 座標を要素として持つ tuple
47 """
---> 48 x, y = coord
49 return "ABC".index(x), int(y) - 1
ValueError: not enough values to unpack (expected 2, got 0)
上記のエラーメッセージは、以下のような意味を持ちます。
-
ValueError
値(value)に関するエラー -
not enough values to unpack (expected 2, got 0)
展開する(to unpack)ために必要な値(value)が 2 つ期待されている(expected)が、0 個(got 0)しかないため足りない(not enough)
エラーの原因の究明
エラーメッセージ から、excel_to_xy
のブロック の中で x, y = coord
の処理を行う際に エラーが発生 していることがわかります。また、この文は、coord
に代入 された、2 文字の Excel 座標 の 1 文字目 を x
に、2 文字目 を y
に 代入 する処理ですが、エラーメッセージ から、coord
に 代入 されている 文字列の長さ が 0 である ことが 推測 されます。
エラーメッセージ から、excel_to_xy
は、test_judge
のブロックの、下記 のプログラムの 部分 から 呼び出されている ので、この部分について 調べてみる必要 がありそうです。
for coord in testdata.split(","):
x, y = excel_to_xy(coord)
上記のプログラムでは、for 文の 反復可能オブジェクト として、testdata.split(",")
が記述 されていますが、このテストケース の場合は、testdata
には、空文字 である ""
が代入 されているので、その状態 で、下記のプログラムのように、testdata.split(",")
を実行 してみます。
testdata = ""
print(testdata.split(","))
実行結果
['']
実行結果からわかるように、testdata.split(",")
によって、空文字 を 要素 として持つ list が得られる ことがわかります。そのため、for coord in testdata.split(","):
を実行すると、coord
に空文字が代入 された状態で、for 文のブロックが実行 され、その 結果 として、excel_to_xy
のブロックの中で x, y = ""
が実行される ため、先ほどの エラーが発生 します。下記のプログラムによって、実際 に x, y = ""
を実行し、同じエラーが発生 することを 確認 できます。
x, y = ""
実行結果
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
c:\Users\ys\ai\marubatsu\031\marubatsu.ipynb セル 4 line 1
----> 1 x, y = ""
ValueError: not enough values to unpack (expected 2, got 0)
split
メソッドに関する注意点
先程のテストケースでは、一つも 着手を行わない ことを表す データ を 空文字で表現 しました。これは、着手するデータ を Excel 座標という 文字列 で ","
で区切って表現 することと、着手 するデータが 存在しない ということから、人間にとって は、自然な表現方法 だと思います。
また、split
メソッド は、特定の文字列 で 区切られた 文字列を、list に変換 する処理を行うので、Excel 座標を ","
で区切って連結 したデータを list に変換 する際に、split
メソッドを利用 するのも 自然な考え方 だと思いますが、そこに今回のエラーの 落とし穴 があります。
この エラーの原因 は、空文字 を split
メソッド で list に変換 すると、空の list に変換 されると 錯覚 してしまった点にあります。実際に、筆者もそのような錯覚 をしていたため、今回の記事を記述するまで、このような バグ が test_judge
に 存在 することに 気が付いていませんでした。
人間は 先入観を持つ 生き物です。関数の説明の 概要から、勝手にその関数が行う処理を 理解したつもりになる ことは良くある事なので、その点に 注意する必要 があります。
上記のように、他人が作成した関数 や プログラム が 実際に行う処理 と、その関数やプログラムを 利用する人 が 期待する処理 が 微妙に異なる ことによって バグが発生 することは、よくある事 です。そのようなことを 避ける ためには、関数やプログラムの 使い方の説明 を よく読んで正確に理解 する必要があります。
可能であれば そうすべきですが、時間などの都合によって、自分が利用する すべての関数 の説明をしっかりと読んで、細部まで理解する ことが 現実的ではない 場合があります。例えば、これまでに紹介した print
などの組み込み関数の説明を細部まで読んで すべて理解 している人は あまり多くない と思います。筆者も他人が作成した関数を利用する際に、それらのすべての関数の説明を熟読し、詳細まで理解して使っているわけではありません。
また、説明を 熟読したとしても、説明 そのものが 間違っていたり、内容が 古かったりする1こともあります。従って、どれだけ注意しても、このようなバグが発生することを 完全に無くす ことは 現実的には無理 だと思いますので、バグが発生した後 で、バグの原因 を 見つけ出して修正する という 技術が重要 になります。
何度も同じようなことを書いているので、本当にくどいと思っている方がいるかもしれませんが、そのような技術を身に付けるためには、そのような バグを体験 し、実際に修正 するという 経験を積む 以外の方法はないと思いますので、本記事ではそのようなバグが発生した場合に、なるべく裏でこっそりと修正した記事を載せるのではなく、正直にそのようなバグが発生したことを記述し、その修正方法まで載せることにしています。
このバグを修正する方法を説明する前に、空文字に対して split
メソッドを実行した際に、空の list に変換されない 理由 について説明します。
例えば、"ABC".split(",")
のように、文字列の中 に1 つも ","
が記述されていない 場合に、split
メソッドの 実引数に ","
を記述 して list に変換 する処理を考えてみて下さい。この場合に、文字列の中に ","
が記述されていないから といって、空の list に変換される と考える人は ほとんどいない のではないでしょうか?実際に、下記のプログラムのように、この処理によって、['ABC']
という、元の文字列 を 要素 とする list に変換 されます。
print("ABC".split(","))
実行結果
['ABC']
空文字 のことを、特別な文字列 であると 考えている 人がいる かもしれません が、"ABC"
も、空文字も、文字列である ことに 変わりはありません。従って、"".split(",")
を実行すると、"ABC".split(",")
と 同様 に、元の文字列 を 要素 とする ['']
という list に変換されます。
文字列型のデータの split
メソッド は、以下の処理 を行う。
-
文字列 を、
split
メソッドの 実引数 に記述した文字列で 区切り、区切られた文字列 を 要素 とする list に変換 する -
文字列の中 に、
split
メソッドの 実引数 に記述した 文字列が存在しない 場合は、元の文字列のみ を 要素 とする list に変換 する -
split
メソッドによって、空の list が 作成 される ことはない
エラーの修正
テストケースに記述した 空文字 を 空の list に 変換したい 場合は、下記のプログラムのように、if 文 を使って、文字列 が 空文字であるか どうかを チェックする 必要があります。下記のプログラムでは、list に変換 したデータを coords
というローカル変数に 代入する ことにし、1 ~ 4 行目の if 文で、空文字の場合 は 空の list を、そうでない場合 はこれまで通り、split
メソッド を使って list に変換 したデータを coords
に代入 しています。また、5 行目で coords
を for 文 の 反復可能オブジェクト として 記述 するように修正しています。
1 if testdata == "":
2 coords = []
3 else:
4 coords = testdata.split(",")
5 for coord in coords:
6 x, y = excel_to_xy(coord)
行番号のないプログラム
if testdata == "":
coords = []
else:
coords = testdata.split(",")
for coord in coords:
x, y = excel_to_xy(coord)
修正箇所
+ if testdata == "":
+ coords = []
+ else:
+ coords = testdata.split(",")
- for coord in testdata.split(","):
+ for coord in coords:
x, y = excel_to_xy(coord)
上記をもっと 簡潔に記述 したい人は、以前の記事 で説明した、三項演算子 を使って、下記のプログラムのように記述すると良いでしょう。本記事ではこのように記述することにします。
coords = [] if testdata == "" else testdata.split(",")
for coord in coords:
x, y = excel_to_xy(coord)
修正箇所
- if testdata == "":
- coords = []
- else:
- coords = testdata.split(",")
+ coords = [] if testdata == "" else testdata.split(",")
for coord in coords:
x, y = excel_to_xy(coord)
coords
を使わずに、下記のように さらに短く記述 することも可能ですが、プログラムが わかりにくくなる という欠点があるので、本記事では 採用しません。
for coord in [] if testdata == "" else testdata.split(","):
x, y = excel_to_xy(coord)
修正箇所
- coords = [] if testdata == "" else testdata.split(",")
- for coord in coords:
+ for coord in [] if testdata == "" else testdata.split(","):
x, y = excel_to_xy(coord)
下記のプログラムは、上記の修正を行った test_judge
です。なお、この関数の中で excel_to_xy
を使用しているので、1 行目でこの関数をインポートしています。
from test import excel_to_xy
def test_judge(testcases):
for testcase in testcases:
testdata, winner = testcase
mb = Marubatsu()
coords = [] if testdata == "" else testdata.split(",")
for coord in coords:
x, y = excel_to_xy(coord)
mb.move(x, y)
print(mb)
if mb.judge() == winner:
print("ok")
else:
print("error!")
修正箇所
def test_judge(testcases):
for testcase in testcases:
testdata, winner = testcase
mb = Marubatsu()
+ coords = [] if testdata == "" else testdata.split(",")
- for coord in testdata.split(","):
+ for coord in coords:
x, y = excel_to_xy(coord)
mb.move(x, y)
print(mb)
if mb.judge() == winner:
print("ok")
else:
print("error!")
修正後の test_judge
を使うことで、下記のプログラムのようにエラーが発生しなくなります。また、実行結果から、正しくテストを行うことができることが確認できます。
test_judge(testcases)
実行結果
Turn o
...
...
...
ok
None
の判定と、is、is not 演算子
実は、この時点で気づいたのですが、test_judge
にはあまりよくない記述があります。それは、test_judge
の下記の部分で、==
演算子 を使って、mb.judge()
の返り値 と、期待される返り値 の 比較 を行っている点です。
if mb.judge() == winner:
print("ok")
else:
print("error!")
mb.judge()
が、数値型 や、文字列型 のデータ のみを返す 場合は、==
演算子 を使った比較で 全く問題はない のですが、mb.judge()
は、ゲームの 決着がついていない 場合は、None
を返す ように定義しています。Python では、None
を ==
演算子で比較 することは 推奨されていません。
==
演算子と同値性
その理由は、==
演算子が、同じ意味 を持つ値であるかどうかを 判定する という処理が行われるからです。そのような性質を 同値性 と呼びます。
意味が分かりづらいと思いますので、具体例を挙げて説明します。これまで 数字 を表すデータの事を、区別する必要がない 場合は 数値型 のように、区別せず に呼んできましたが、Python では、0
のような 整数 のデータは 整数型(int)、1.2
のような 小数点を含む データは 浮動小数点数型(float)という、異なるデータ型 で表現されます。そのため、Python では、整数型 と 浮動小数点数型 の、2 種類 のデータ型で 0
という数値を 表現 することができます。
Python では、数値に 小数点をつけて記述 することで、浮動小数点数型 のデータを表現するという 決まり になっているので、0
は整数型、0.0
は浮動小数点数型 のデータです。そのことは、データ型 を返す、組み込み関数 type
2 を使って下記のプログラムのように確認できます。
print(type(0))
print(type(0.0))
実行結果
<class 'int'>
<class 'float'>
Python では、0
と 0.0
は、異なるデータ型 のデータですが、その 意味 は 同じゼロ です。先程説明したように、==
は 同じ意味 のデータであるかどうかを表す 同値性を判定 する演算子なので、下記のプログラムのように 0
と 0.0
を ==
で比較 すると True
になります。
print(0 == 0.0)
実行結果
True
異なるデータ型 のデータを ==
演算子 で 比較 した場合に 行われる処理 はデータ型によって異なります。そのため、「None
」と、「None
ではないが None
と同じ意味 を持つデータ」を ==
演算子 で 比較 すると True
になる 場合 があります。これが、None
であるかどうか を ==
で比較 することが __推奨されない理由__です。
なお、等しくない ことを 判定 する !=
演算子 についても 同様の理由 で、None
でない ことを 判定 するために使うことは 推奨されません。
is 演算子と同一性
データが 完全に同じ ものであることを、同一性 と呼び、Python では、is 演算子 によって 前後のデータ が 同一 であるかどうかを 判定 することができます。
下記のプログラムのように、0
と 0.0
は、同じ意味を持ちますが、異なるデータ なので、is 演算子 を使って 比較 すると、計算結果が False
になります。
a = 0
b = 0.0
print(a == b)
print(a is b)
実行結果
True
False
同様 の演算子に、同一でない ことを判定する演算子として is not
という 演算子 があります。is not
で 一つの演算子 である点に注意して下さい。
is 演算子の注意点
Python の is 演算子 は、データを管理する オブジェクトが等しい かどうかによって 同一性 を 判定 します。従って、以前の記事 で説明したように、全く同じ数値 データであっても、オブジェクトの id
が 異なる ような場合に、is 演算子で比較 を行うと、下記のプログラムのように、そのデータを管理する オブジェクトが異なる という理由で False
になる場合 があります。従って、is 演算子 を 数値型 や、文字列型 どうしの 比較 を行う際に 使うべきではありません。
a = 1.23
b = 1.23
print(a is b)
実行結果(OS や Python のバージョンによっては True
になる場合があります)
False
初心者のうちは、is 演算子 は、None
であるかどうかを判定 する場合 のみで使う と覚えておき、必要になってから他の is 演算子の用途について学べば十分だと思います。
プログラムの修正
先程のプログラムの修正方法の一つは、下記のプログラムのように、テストケースの 期待される値 が None
の場合 は、is
演算子 を、それ以外の場合 は ==
演算子 を使って比較を行います。
if winner is None:
if mb.judge() is winner:
print("ok")
else:
print("error!")
else:
if mb.judge() == winner:
print("ok")
else:
print("error!")
上記のプログラムは、無駄が多い ので、and と or 演算子 を使って下記のように記述することができます。ただし、何れの方法を使っても、判定の処理が複雑になる ことには変わりはありません。
if (winner is None and mb.judge() is winner) or \
(winner is not None and mb.judge() == winner):
print("ok")
else:
print("error!")
別の方法 として、決着がついていない場合 に、judge
メソッドの 返り値 として、None
以外 のデータを 返す ように修正するという方法があります。この方法であれば、test_judge
を変更 する 必要はなくなる ので、本記事では、その方法を採用することにします。
この方法を採用する場合は、決着がついていない場合 に、judge
メソッドの 返り値 に 何を返すか を 決める 必要があります。決着がついていない場合を 除くと、judge
メソッドが 返すデータ は、下記のプログラムのように、Marubatsu
クラスの先頭に、クラス属性として定義 していました。
class Marubatsu:
EMPTY = "."
CIRCLE = "o"
CROSS = "x"
DRAW = "draw"
そこで、決着がついていない場合 を表す値も 同様 に、クラス属性 として 定義 する事にします。「決着がついていない」ということは、「ゲームをプレイ中」であると 言い換える ことが出来るので、クラス属性の名前を PLAYING
という名前にすることにします。また、その値 は、他のクラス属性と 区別 できれば なんでも構わない ので、DRAW
と同様に、"playing"
という 文字列で表現 することにします。そこで、下記のプログラムのように、Marubatsu
クラス の クラス属性 に、PLAYING
を追加 します。
Marubatsu.PLAYING = "playing"
次に、テストケース を下記のように 修正 します。
testcases = [
# ゲーム盤に一つもマークが配置されていない場合のテストケース
[
"",
Marubatsu.PLAYING,
],
]
修正した test_judge
を使ってテストを実行すると、筆者もうまくいくと思っていたのですが、実行結果に error! が表示 されてしまいます。その理由について少し考えてみて下さい。
test_judge(testcases)
実行結果
Turn o
...
...
...
error!
error!
が表示 されるのは、test_judge
のブロックの 下記の部分 です。従って、mb.judge()
の返り値 か、winner
の値 の いずれか 、または その両方 が間違っている 可能性が高い でしょう。
if mb.judge() == winner:
print("ok")
else:
print("error!")
これまでのエラー の多くは、プログラムが停止 するようなエラーで、その際に表示される エラーメッセージ は、Python が作成 した 詳細 なもので、それを頼り にエラーを 修正 してきました。
一方、今回のエラー は、プログラムが停止 するようなエラー ではなく、表示される エラーメッセージ は、自分で何を表示するか を print
で記述 することで表示されたものです。このエラーメッセージは、エラーがあることしか説明 していないので、このメッセージから mb.judge()
と winner
の どちらが間違っているか を 推測 することは できません。この 問題 は、エラーの メッセージ に、エラーの 原因を特定 できるような 情報が含まれていない ことが原因です。従って、下記のプログラムのように、エラーの原因 となる、mb.judge()
と winner
の値 を エラーメッセージに含める ように修正することで、メッセージから エラーの原因 が 推測しやすく なります。
なお、下記のプログラムで表示される エラーメッセージ は 一例 です。これより詳しいエラーメッセージを表示したい場合は、print
で表示する内容を 自由に修正 して下さい。
def test_judge(testcases):
for testcase in testcases:
testdata, winner = testcase
mb = Marubatsu()
for coord in [] if testdata == "" else testdata.split(","):
x, y = excel_to_xy(coord)
mb.move(x, y)
print(mb)
if mb.judge() == winner:
print("ok")
else:
print("test_judge error!")
print("mb.judge():", mb.judge())
print("winner: ", winner)
修正箇所
def test_judge(testcases):
for testcase in testcases:
testdata, winner = testcase
mb = Marubatsu()
for coord in [] if testdata == "" else testdata.split(","):
x, y = excel_to_xy(coord)
mb.move(x, y)
print(mb)
if mb.judge() == winner:
print("ok")
else:
- print("error!")
+ print("test_judge error!")
+ print("mb.judge():", mb.judge())
+ print("winner: ", winner)
修正後に test_judge
を実行すると、以下のような実行結果が表示されます。
test_judge(testcases)
実行結果
Turn o
...
...
...
test_judge error!
mb.judge(): None
winner: playing
実行結果から、期待される値 である winner
には 正しい "playing"
が 代入 されていますが、mb.judge()
の返り値 が None
のまま で 間違っている ことがわかります。原因 が、judge
メソッドにある ことがわかったので、関数の定義 を 調べてみる、下記のプログラムのように、最初に winner
に None
を代入したまま であることがわかります。
def judge(self):
# 判定を行う前に、決着がついていないことにしておく
winner = None
略
従って、このバグは、下記のプログラムのように、judge
メソッドの最初で、winner
に Marubatsu.PLAYING
を代入 するようにすることで修正することが出来ます。なお、長くなるので下記には judge
メソッドの先頭の一部のみ記述しますが、github にアップロードする marubatsu.ipynb のほうでは、全体を記述します。
def judge(self):
# 判定を行う前に、決着がついていないことにしておく
winner = Marubatsu.PLAYING
略
Marubatsu.judge = judge
修正箇所
def judge(self):
# 判定を行う前に、決着がついていないことにしておく
- winner = None
+ winner = Marubatsu.PLAYING
下記のプログラムを実行することで、修正した test_judge
が正しく動作することが確認できます。実行結果は先程と同様になるので省略します。
test_judge(testcases)
決着がついていないことを表すデータを、None
から、Marubatsu.PLAYING
に修正したことで、judge
メソッドのローカル変数 winner
の名前 と、その変数に 代入するデータ の 整合性がとれなくなります。具体的には、勝者を表す winner
という名前の変数に、Marubatsu.PLAYING
という、ゲームの 勝者 と 無関係 なデータが代入される 可能性が生じます。このような場合は、winner
という変数の名前を別のより ふさわしい名前に変更 することも考えられますが、この問題は 些細な問題 であると考えて、変数の名前を 修正しない という方法も考えられます。本記事では、このままでもプログラムの意味が著しくわかりにくくなることは無いと思いましたので、修正しないことにします。
1 つだけが True
になるテストケース
次に、or 演算子 で 連結 された 式 の中の 1 つだけ が True
になるテストケースを考えます。
〇の勝利を判定する if 文の条件式の中の、or 演算子で連結された それぞれ の 式が True
になるのは、下図 のマスに 〇 が配置された場合 です。下図の 8 通り の いずれの場合 でも、同時 に 2 つ以上 の場所で 〇 が 3 つ並ぶことはない ので、1 つだけ が True
になる ことが 保証 されます。従って、下図 のように 〇 が配置 された、8 つ のテストケースを 用意すれば良い ことがわかります。
次に、その 8 つのテストケース に対する judge
メソッド の 期待される返り値 について考える必要があります。〇 を 3 つ配置 するということは、その間 に × は 2 回 しか 配置 しないことになるので、用意するテストケースで どこに × を配置 しても、× が勝利 することは ありません 。従って、× の勝利を判定 する if 文の 条件式 の計算結果は 必ず False
になります。
テストケースでは、着手 は 〇 の 3 回、× の 2 回 の、計 5 回 になるので、引き分け になることは ありません。従って、引き分けを判定 する if 文の 条件式 の計算結果も 必ず False
になります。
上記の事から、× は 空いているマスであれば、どのマスに配置しても良い こともわかります。
下図は、上記の テストケース の一つに対する judge
メソッド の 処理の流れ を表す フローチャート で、赤い枠線と線 が行われる 処理の流れ を表します。図から、winner
に関する 代入処理 は、赤字 の winner = Marubatsu.CIRCLE
が 最後に行われる ので、judge
メソッドの 期待される返り値 は Marubatsu.CIRCLE
であることがわかります。
下記のプログラムは、先程のテストケースに、1 つだけ が True
になる テストケースを 3 つ加えた ものです。8 つすべてを加えない理由についてはこの後で説明します。
なお、これまで はそれぞれのテストケースの 盤面 を コメントで記述 していましたが、テストケースを 数多く入力 際に、そのような コメント を毎回 記述するのは大変 なので以降は記述しません。
testcases = [
# ゲーム盤に一つもマークが配置されていない場合のテストケース
[
"",
Marubatsu.PLAYING,
],
# 〇の勝利のテストケース
[
"A1,A2,B1,B2,C1",
Marubatsu.CIRCLE
],
[
"A2,A1,B2,B1,C2",
Marubatsu.CIRCLE
],
[
"A3,A1,B3,B1,C3",
Marubatsu.CIRCLE
],
]
このテストケースで test_judge
でテストを行うと以下のような実行結果が表示されます。
test_judge(testcases)
実行結果
Turn o
...
...
...
ok
Turn x
ooo
xx.
...
ok
Turn x
xx.
ooo
...
ok
Turn x
xx.
...
ooo
ok
上記のテストデータを実際に入力する際に、入力した座標 が 正しいか どうかが 自信が持てない 人が多いのではないかと思います。筆者も 入力する際に間違った入力をしていることに気づいて 何度か修正 しました。また、入力したデータ が 正しいか どうかの 確認 を、入力したデータ そのもの を 目で見て確認 することは、Excel 座標にかなり慣れないと 困難です。
test_judge
が行う 処理の中 で、テストケースのデータを元に着手が行われた ゲーム盤を画面に表示 するようにしたのは、入力 した テストケース が 正しいか どうかを、わかりやすく 確認できるようにする ためです。テストケースのデータを入力し、test_judge
を実行した際は、必ず実行結果を見て確認するようにして下さい。
上記の実行結果で表示される ゲーム盤の表示内容 から、意図通り のテストケースが 記述されている ことが 確認 できます。また、すべて のテストケースで ok
が表示 されているので、入力したテストケース に対して judge
メソッド が 期待された処理 を行うことが 確認 できます。
test_judge
は、judge
メソッド が 期待される処理を行う かどうかを 確認 する だけでなく、入力したテストケース が 正しい かどうかを 確認 する 役割 を持つ。
期待される返り値ごとに、テストケースをまとめる方法
先程のテストケースに、8 つ のテストケースを すべて追加しなかった理由 について説明します。
下記の先程のテストデータをよく見て下さい。無駄な記述 があると思う人はいないでしょうか?
testcases = [
# ゲーム盤に一つもマークが配置されていない場合のテストケース
[
"",
Marubatsu.PLAYING,
],
# 〇の勝利のテストケース
[
"A1,A2,B1,B2,C1",
Marubatsu.CIRCLE
],
[
"A2,A1,B2,B1,C2",
Marubatsu.CIRCLE
],
[
"A3,A1,B3,B1,C3",
Marubatsu.CIRCLE
],
]
上記のテストケースのデータ構造には、それほど大きな欠点とは言えないかもしれませんが、いくつかの問題があるため、改良の余地 があります。
問題点の一つは、3 つ の 〇 が勝利するテストケースの それぞれ に、Marubatsu.CIRCLE
が記述 されている点です。これは、今後 〇 が勝利 する場合のテストケースを 記述するたび に、Marubatsu.CIRCLE
を記述 する 必要がある ということを意味しますが、冗長 だと思いませんか?
もう一つの問題点は、異なる judge
メソッドの 期待される返り値 を持つ テストケース が、1 つの list で まとめられている という点です。現時点 では、〇 の勝利 のテストケースを 表す list の 要素 が 隣り合って まとまっていますが、今後 この list に、他の期待される返り値 を持つテストケースを 追加 した際に、同じ期待される返り値 を持つテストケースが、list の中で バラバラに配置 されてしまう 可能性が高く なります。もちろん、バラバラにならないように気をつけながらデータを記述することもできますが、いちいちそのようなことを 注意しながら データを 記述 するのは 面倒 ですし、間違って バラバラになるように 記述 してしまった 場合 に、それをバラバラにならないように 記述し直す のは 大変 です。
judge
メソッドをテストするテストケースには、4 種類 の 期待される返り値 が存在します。また、その 4 種類の 期待される返り値 に 対して 、それぞれ複数 のテストケースを 記述 することができます。このことを、別の言葉で表現すると、1 つ の 期待される返り値 から、複数のテストデータ への 対応づけ が行われることを意味します。また、期待される返り値 は 文字列 で表現されるので、この 対応づけ は、dict のキー に 期待される返り値 を、その キーの値 に 対応 する 複数 の テストデータ を 要素 とする list を 代入 するという データ構造 で表現することができます。
具体的には、下記のプログラムのようにテストケースを記述することができます。なお、データが list から dict に変わった ので、データを 囲う記号 を []
から {}
に修正 する必要があります。
testcases = {
# ゲーム盤に一つもマークが配置されていない場合のテストケース
Marubatsu.PLAYING: [
"",
],
# 〇の勝利のテストケース
Marubatsu.CIRCLE: [
"A1,A2,B1,B2,C1",
"A2,A1,B2,B1,C2",
"A3,A1,B3,B1,C3",
],
}
このデータ構造は、先程のデータ構造と異なり、期待される返り値 を表すデータを dict のキー として 1 箇所だけに記述 するので、その分だけ 記述が簡潔 になります。
また、同じ 期待される返り値に 対応するテストデータ が、同じ list の中に 集まっている ので、それぞれの テストデータの意味 が わかりやすくなります。
さらに、テストケース の全体の 記述の行数 も、大幅に短く することができます。
このように、新しいデータ構造には様々な利点があるので、本記事では以降はこのデータ構造でテストケースを記述することにします。
本記事では、以降はテストケースのデータ構造を以下のように記述する。
- dict で表現する
- dict の キー は、
judge
メソッド の 期待される返り値 を表す - キーの値 には、キーの値に 対応する テストデータを 要素とする list を 代入 する
テストケースを、list を使わず に、下記のように記述すれば良いと思った人はいないでしょうか?残念ながら、下記のプログラムのように、dict のデータを 記述 する際に、同じキー を 何度も記述 すると、実行結果からわかるように、最後に記述 された キーの値のみ が 代入 されることになります。なお、下記のプログラムでは、先程 testcases
に代入したデータを壊さないようにするために、testcases2
という変数に値を代入しています。
testcases2 = {
# ゲーム盤に一つもマークが配置されていない場合のテストケース
Marubatsu.PLAYING: "",
# 〇の勝利のテストケース
Marubatsu.CIRCLE: "A1,A2,B1,B2,C1",
Marubatsu.CIRCLE: "A2,A1,B2,B1,C2",
Marubatsu.CIRCLE: "A3,A1,B3,B1,C3",
}
print(testcases2)
実行結果
{'playing': '', 'o': 'A3,A1,B3,B1,C3'}
上記のような処理が行われる 理由 は、上記のプログラムを実行すると、下記のプログラムのような処理が行われるからです。dict のデータを記述して実行すると、下記のプログラムのように、空の dict が 最初に作成 され、その後で、記述された順番通り に、dict のキー に 値が代入 されます。従って、上記のプログラムは、下記のように、同じキー に対する 代入処理 が 何度も行われ、そのたびに キーの値 が 上書き されることになります。
testcases2 = {}
testcases2[Marubatsu.PLAYING] = ""
testcases2[Marubatsu.CIRCLE] = "A1,A2,B1,B2,C1"
testcases2[Marubatsu.CIRCLE] = "A2,A1,B2,B1,C2"
testcases2[Marubatsu.CIRCLE] = "A3,A1,B3,B1,C3"
print(testcases2)
実行結果
{'playing': '', 'o': 'A3,A1,B3,B1,C3'}
test_judge
の修正
テストケースの データ構造が変わった ので、それに合わせて test_judge
を修正 する必要があります。test_judge
に対する修正は以下の通りです。
- 2 行目を
for winner, testdata_list in testcases.items():
に修正する - 元のプログラムにあった
testdata, winner = testcase
を削除 する - 3 行目を、
for testdata in test_data_list:
に修正し、残り のプログラムの インデントを右にずらす ことで、この for 文のブロック の プログラムになる ように修正する
2 行目では、以前の記事 で説明した、for 文で dict の キーと値の両方を取り出す繰り返し を記述しています。testcases
のキー は、judge
メソッドの 期待される返り値 を表すデータです。従って、このデータを winner
に 代入 することで、修正前と同じ処理 を行うことができます。また、この修正により、元の testdata, winner = testcase
は 必要が無くなった ので 削除 しています。
testcases
の キーの値 は、キーに対応 する テストデータの list なので、その値を testdata_list
という名前の 変数に代入 しています。そして、3 行目の for 文で、testdata_list
の 先頭の要素 から 順番に取り出し て testdata
に代入 するという 繰り返し処理 を記述することで、2 行目で testcases
から取り出した、winner
に対応 するテストデータの テスト を まとめて行う ことができます。なお、3 行目以降のプログラムは、修正する前 の judge
メソッドのプログラムと、インデントが変わった点を除けば、同一の内容 になります。
1 def test_judge(testcases):
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 print(mb)
9
10 if mb.judge() == winner:
11 print("ok")
12 else:
13 print("test_judge error!")
14 print("mb.judge():", mb.judge())
15 print("winner: ", winner)
行番号のないプログラム
def test_judge(testcases):
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 mb.judge() == winner:
print("ok")
else:
print("test_judge error!")
print("mb.judge():", mb.judge())
print("winner: ", winner)
修正箇所
4 行目以降 の for 文のブロックには インデントを追加 していますが、それらの行の修正を示すと、かえってわかりづらく 3 行目以降の修正は示しません。
def test_judge(testcases):
- for testcase in testcases:
+ for winner, testdata_list in testcases.items():
- testdata, winner = testcase
+ 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 mb.judge() == winner:
print("ok")
else:
print("test_judge error!")
print("mb.judge():", mb.judge())
print("winner: ", winner)
下記のプログラムを実行することで、修正した test_judge
が正しく動作することが確認できます。実行結果は先程と同様になるので省略します。
test_judge(testcases)
残りのテストケースの記述
下記は、新しいデータ構造 で、〇 が勝利 した場合の テストケースを記述 したプログラムです。
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",
],
}
下記のプログラムを実行することで、修正した 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
今回の記事のまとめ
今回の記事では、MC/DC テスト の中で、〇 の勝利を判定 する if 文 に対する テストケースを記述 し、テストを行いました。その際に、テストケース を表す データ構造 の 改良 を行いました。
もちろん、MC/DC テストに限らず、完璧なテスト を行うことは 不可能 なので、バグが絶対に存在しない ことまでは 保証できません が、judge
メソッドの中で、〇の勝利を判定 する if 文 に関しては、バグが存在する 可能性が低い ということは言えます。
本当は今回の記事でテストを終わらせる予定だったのですが、筆者も気づいていなかった split
メソッドに関するバグが判明した点や、引き分けの場合の None
に関する judge
メソッドの返り値の問題点など、想定外の説明を行う必要がでたため、テストの残りは次回に続くことにします。
本記事で入力したプログラム
以下のリンクから、本記事で入力して実行した JupyterLab のファイルを見ることができます。
以下のリンクは、今回の記事で更新した marubatsu.py です。
以下のリンクは、今回の記事で更新した test.py です。
次回の記事