目次と前回の記事
実装の進捗状況と前回までのおさらい
〇×ゲームの仕様と進捗状況
正方形で区切られた 3 x 3 の 2 次元のゲーム盤上でゲームを行う
ゲーム開始時には、ゲーム盤のすべてのマスは空になっている
2 人のプレイヤーが遊ぶゲームであり、一人は 〇 を、もう一人は × のマークを受け持つ
2 人のプレイヤーは、交互に空いている好きなマスに自分のマークを 1 つ置く
先手は 〇 のプレイヤーである
- プレイヤーがマークを置いた結果、縦、横、斜めのいずれかの一直線の 3 マスに同じマークが並んだ場合、そのマークのプレイヤーの勝利とし、ゲームが終了する
- すべてのマスが埋まった時にゲームの決着がついていない場合は引き分けとする
仕様の進捗状況は、以下のように表記します。
- 実装が完了した部分を
背景が灰色の長方形
で記述する - 実装の一部が完了した部分を、太字 で記述する
これまでに作成したモジュール
以下のリンクから、これまでに作成したモジュールを見ることができます。
前回までのおさらい
前回の記事では、命令網羅(C0) で judge
メソッドのテストを開始しました。
最初に 〇 が勝利した場合のテストケースでテストし、その後で、今後のテストを効率よく行うための test_judge
という関数を定義しました。
judge
メソッドのテスト
命令網羅(C0) によるテスト(続き)
× が勝利した場合のテスト
× が勝利した場合のテストも、〇 が勝利した場合のテストと同様に、下図右のような、0 行に 3 つ × が並んだ状態のテストケースで行うことにします。
下記は、上図左の順番でマークを配置するためのテストケースを表すデータを testcase
に代入し、test_judge
を呼び出すことでテストを行うプログラムです。実行結果からわかるように、表示されるゲーム盤は 確かに 0 行に 3 つ × が並んでいます が、その下に error!
が表示されてしまいます。その原因について考えてみて下さい。
from marubatsu import Marubatsu
from test import test_judge
testcase = [
[ 0, 1 ], # 着手順とゲーム盤
[ 0, 0 ], # 246 xxx
[ 1, 1 ], # 13. oo.
[ 1, 0 ], # 5.. o..
[ 0, 2 ],
[ 2, 0 ],
]
test_judge(testcase)
実行結果
Turn o
xxx
oo.
o..
error!
エラーが発生する原因は、test_judge
の 7 行目の if 文が、if mb.judge() == Marubatsu.CIRCLE:
のように、mb.judge()
の返り値 が 〇 の勝利 を表す
Marubatsu.CIRCLE
と等しいことを 判定 しているため、〇 の勝利 の場合は "ok"
を、そうでない 場合は "error!"
を表示するようになっていることです。今回のテストでは × の勝利を判定 したいので、この部分を修正する必要があります。
ただし、7 行目を if mb.judge() == Marubatsu.CROSS:
のように書き換えてしまうと、今度は 〇 の勝利の判定 に test_judge
を 使うことが出来なく なってしまいます。
テストケースを表すデータ構造の修正 その1
この問題は、testcase
の中に、テストデータしか 代入されていないことが原因です。以前の記事 で説明したように、テストケースは、テストデータ と、期待される処理 の 組み合わせ であることを思い出してください。
そこで、testcase
の中に、期待される処理 を表す データを組み込む ように、テストケースを表す データ構造を修正 することにします。期待される処理を表すデータは、judge
メソッドの返り値 なので、例えば、先程の × が勝利 する testcase
の中に、Marubatsu.CROSS
のデータを組み込む必要があります。そのために、どのようなデータ構造にすれば良いかについて少し考えてみて下さい。
なお、前回の記事 では、『テストで利用する「特定のマスにマークが配置された Marubastu
クラスのインスタンス」のことを、単に テストケース と表記します』のように説明しましたが、以後は 、上記のデータに、期待される処理を表さすデータ を 組み込んだもの のことを テストケース と表記することにします。同様に、テストケースを表すデータ も、期待される処理を表すデータ を 組み込んだもの とします。
簡単に思いつく方法として、下記のプログラムのように、現状の testcase
を表す list の 最後の要素 に、期待される処理を表すデータを 追加する という方法があるでしょう。
同様の方法として、先頭の要素 に追加するという方法も考えられます。
testcase = [
[ 0, 1 ], # 着手順とゲーム盤
[ 0, 0 ], # 246 xxx
[ 1, 1 ], # 13. oo.
[ 1, 0 ], # 5.. o..
[ 0, 2 ],
[ 2, 0 ],
Marubatsu.CROSS, # 最後の要素が、期待される処理を表す
]
testcase
の データ構造が変化 したので、test_judge
も それに合わせて修正 する必要があります。具体的には、期待された処理 を表す testcase
に代入された list の 最後の要素 を 取り出して、そのデータ を使って テストの判定を行う ように修正します。
そのような処理は、list の 最後の要素を削除 し、返り値として返す、pop
1 という list のメソッド を利用することで記述することが出来ます。
pop
メソッドの具体的な使用例を挙げます。下記のプログラムは、1 行目で、a
に [1, 2, 3]
という list を代入し、2 行目で、pop
メソッドを呼び出して返り値を last
に代入しています。実行結果から、last
には、a
の 最後の要素 であった 3
が 代入 され、a
に代入された list から、最後の要素が削除 されていることがわかります。
a = [1, 2, 3]
last = a.pop()
print(last)
print(a)
実行結果
3
[1, 2]
tuple は イミュータブル なデータなので、要素を変化 させる pop
メソッドを 利用 することは できません。tuple を使ってテストケースを表すデータを表現した場合は、後述のノートで説明する方法で、test_judge
を修正する必要があります。
下記は、pop
メソッドを使って test_judge
を修正したプログラムです。2 行目で pop
メソッドを使って、testcase
に代入された list の 最後の要素 を取り出して winner
に 代入 しています。その結果、testcase
の値は、期待される処理の値 を 組み込む前 のデータに 戻る ので、3 ~ 6 行目のプログラムを 修正する必要はありません。
8 行目では、judge
メソッドの返り値を判定する条件式を、先程 winner
に代入した 期待される処理 を表すデータを 使って判定 するように修正しています。
1 def test_judge(testcase):
2 winner = testcase.pop()
3 mb = Marubatsu()
4 for x, y in testcase:
5 mb.move(x, y)
6 print(mb)
7
8 if mb.judge() == winner:
9 print("ok")
10 else:
11 print("error!")
行番号のないプログラム
def test_judge(testcase):
winner = testcase.pop()
mb = Marubatsu()
for x, y in testcase:
mb.move(x, y)
print(mb)
if mb.judge() == winner:
print("ok")
else:
print("error!")
修正箇所
def test_judge(testcase):
+ winner = testcase.pop()
mb = Marubatsu()
for x, y in testcase:
mb.move(x, y)
print(mb)
- if mb.judge() == Marubatsu.CIRCLE:
+ if mb.judge() == winner:
print("ok")
else:
print("error!")
下記のプログラムの実行結果から、正しく動作することが確認できます。
test_judge(testcase)
実行結果
Turn o
xxx
oo.
o..
ok
testcase
を表す list の 先頭の要素 に、期待される処理を表すデータを追加した場合も、pop
メソッドを利用して test_judge
を修正することが出来ます。
pop
メソッドは、実引数に整数 を記述することで、実引数 で記述した インデックスの要素を削除 し、返り値として返す処理を行います。先頭の要素の インデックスは 0
なので、先頭の要素に、期待される処理を表すデータを追加した場合は、test_judge
の 2 行目を winner = testcase(0)
のように修正します。
pop
メソッドの詳細は、下記のリンク先を参照して下さい。
上記の修正方法の欠点
list の pop
メソッド は、list の 要素を削除 してしまうので、testcase
の 内容が変化 してしまうという 欠点 があります。具体的には、下記のプログラムのように、test_judge
を呼び出す 前後 で testcase
の内容を 表示 すると、実行結果から、test_judge
を呼び出した 後 で、testcase
の 値が変化する ことが確認できます。
testcase = [
[ 0, 1 ], # 着手順とゲーム盤
[ 0, 0 ], # 246 xxx
[ 1, 1 ], # 13. oo.
[ 1, 0 ], # 5.. o..
[ 0, 2 ],
[ 2, 0 ],
Marubatsu.CROSS, # 最後の要素が、期待される処理を表す
]
print("befor test_judge:", testcase)
test_judge(testcase)
print("after test_judge:", testcase)
実行結果
befor test_judge: [[0, 1], [0, 0], [1, 1], [1, 0], [0, 2], [2, 0], 'x']
Turn o
xxx
oo.
o..
ok
after test_judge: [[0, 1], [0, 0], [1, 1], [1, 0], [0, 2], [2, 0]]
そのため、上記のプログラムを 実行した後 で、下記のプログラムのように、もう一度 test_judge
を呼び出すと、先程の test_judge
の呼び出しによって、testcase
の値が 変化した ため、今度は "error!" が実行結果に 表示 されてしまいます。
また、気づきにくいかもしれませんが、右上の (2, 0) のマスに × が配置されなく なります。これは、test_judge
の 2 行目で testcase.pop()
が実行された結果、右上の (2, 0) のマスを表す testcase
の 最後の要素の [2, 0]
が削除 されるからです。
test_judge(testcase)
実行結果
Turn x
xx.
oo.
o..
error!
test_judge
を呼び出しても testcase
の値が 変化しないようにする ために、pop
メソッドを使わない方法で test_judge
を修正することは可能です(その方法はこの後のノートで紹介します)が、テストケースに必要なデータ と、期待される処理を表すデータ を、同じ list の要素の中に 混在させる ようなデータ構造は、バグの原因 になる 可能性が高くなります。その理由については、この後で説明します。
修正その 1 のデータ構造は、簡単に思いつく ことが出来る 可能性が高い ので紹介しましたが、本記事では、修正その 1 の修正方法は採用せず、次に説明する 別の方法 でテストケースを表すデータ構造を修正することにします。
バグの原因になる可能性が高い、修正その 1 のデータ構造を紹介した他の理由は、同じデータ に対して 様々なデータ構造で表現する方法 を知っておくことが、プログラミングを行う上で 重要になる からです。また、修正その 1 のようなデータ構造が、常に悪い データ構造になる わけではありません ので、体験することは 無駄にはなりません。
list の スライスという表記 などを用いることで、test_judge
を呼び出しても testcase
の値が 変化しない ように修正することができます。記事が長くなるので今回の記事では下記のプログラムの意味は説明しません(コメントに簡単な説明を入れておきました)が、list のスライスなどは実際に 良く使われる ので必要になった時点で詳しく紹介することにします。
pop
メソッドが利用できない tuple の場合も、下記の方法で修正できます。
def test_judge(testcase):
winner = testcase[-1] # 最後の要素を winner に代入する
testcase = testcase[:-1] # 最後の要素を除いたものを testcase に代入する
mb = Marubatsu()
for x, y in testcase:
mb.move(x, y)
print(mb)
if mb.judge() == winner:
print("ok")
else:
print("error!")
修正箇所
def test_judge(testcase):
- winner = testcase.pop()
+ winner = testcase[-1] # 最後の要素を winner に代入する
+ testcase = testcase[:-1] # 最後の要素を除いたものを testcase に代入する
mb = Marubatsu()
for x, y in testcase:
mb.move(x, y)
print(mb)
if mb.judge() == winner:
print("ok")
else:
print("error!")
下記の実行結果から、test_judge
が正しく動作することと、test_judge
を呼び出しても testcase
の値が変化しないことが確認できます。
testcase = [
[ 0, 1 ], # 着手順とゲーム盤
[ 0, 0 ], # 246 xxx
[ 1, 1 ], # 13. oo.
[ 1, 0 ], # 5.. o..
[ 0, 2 ],
[ 2, 0 ],
Marubatsu.CROSS, # 最後の要素が、期待される処理を表す
]
print("befor test_judge:", testcase)
test_judge(testcase)
print("after test_judge:", testcase)
実行結果
befor test_judge: [[0, 1], [0, 0], [1, 1], [1, 0], [0, 2], [2, 0], 'x']
Turn o
xxx
oo.
o..
ok
after test_judge: [[0, 1], [0, 0], [1, 1], [1, 0], [0, 2], [2, 0], 'x']
なお、スライスに関する詳細については、下記のリンク先を参照して下さい。
テストケースを表すデータ構造の修正 その2
次に、テストケースを表すデータ構造の中に、着手を表すデータと、期待される処理を表すデータを 別々に記録する 方法を紹介します。
ただし、そのような方法は、既に紹介しています ので、新しいことを学ぶ必要はありません。どのようなデータ構造になるかについて少し考えてみて下さい。
先ほど、座標を表す 2 つのデータを、[0, 1]
のように、list の要素として別々に代入しました。それと同様の方法で、list を使って下記のようなデータ構造で、テストケースを表すデータを記述することができます。
- list の 0 番の要素 に、着手を表すデータ を代入する
- list の 1 番の要素 に、期待される
judge
メソッドの 返り値 の値を代入する
本記事では list を使いますが、tuple を使っても構いません。また、上記の list の要素の順番を入れ替えても構いません。
下記は、上記の修正したデータ構造で testcase
に、0 行に × が 3 つ並んだテストケースを代入するプログラムです。
testcase = [
[
[ 0, 1 ], # 着手順とゲーム盤
[ 0, 0 ], # 246 xxx
[ 1, 1 ], # 13. oo.
[ 1, 0 ], # 5.. o..
[ 0, 2 ],
[ 2, 0 ],
],
Marubatsu.CROSS, # 期待される judge メソッドの返り値
]
test_judge
の修正は、2 行目と 4 行目を以下のように追加、修正するだけで済みます。
2 行目では、testdata
と winner
という変数に、testcase
の 0 番と 1 番の要素を代入 しています。testcase
の 中身を変更するのではなく、testdata
と winner
という 異なる変数 に testcase
の 要素を代入 することで、pop
メソッドを使った場合のように、testcase
の 中身が変化 することは 無くなります。
修正前のプログラムでは、testcase
に着手するマスの座標のデータが代入されていましたが、修正後 は、testdata
という名前の変数に そのデータが代入 されるようになったので、4 行目では、testcase
を testdata
に修正 しています。
1 def test_judge(testcase):
2 testdata, winner = testcase
3 mb = Marubatsu()
4 for x, y in testdata:
5 mb.move(x, y)
6 print(mb)
7
8 if mb.judge() == winner:
9 print("ok")
10 else:
11 print("error!")
行番号のないプログラム
def test_judge(testcase):
testdata, winner = testcase
mb = Marubatsu()
for x, y in testdata:
mb.move(x, y)
print(mb)
if mb.judge() == winner:
print("ok")
else:
print("error!")
修正箇所
def test_judge(testcase):
- winner = testcase.pop()
+ testdata, winner = testcase
mb = Marubatsu()
- for x, y in testcase:
+ for x, y in testdata:
mb.move(x, y)
print(mb)
if mb.judge() == winner:
print("ok")
else:
print("error!")
下記のプログラムの実行結果から、正しく動作することが確認できます。また、先程と異なり、test_judge
を呼び出しても、testcase
の値は 変化しません。
testcase = [
[
[ 0, 1 ], # 着手順とゲーム盤
[ 0, 0 ], # 246 xxx
[ 1, 1 ], # 13. oo.
[ 1, 0 ], # 5.. o..
[ 0, 2 ],
[ 2, 0 ],
],
Marubatsu.CROSS, # 期待される judge メソッドの返り値
]
print("befor test_judge:", testcase)
test_judge(testcase)
print("after test_judge:", testcase)
befor test_judge [[[0, 1], [0, 0], [1, 1], [1, 0], [0, 2], [2, 0]], 'x']
Turn o
xxx
oo.
o..
ok
after test_judge [[[0, 1], [0, 0], [1, 1], [1, 0], [0, 2], [2, 0]], 'x']
list の用途と、修正その 1 を採用しない理由
修正その 2 のデータ構造は、list の要素の中に 異なる種類のデータが代入 されているので、先程の修正その 1 のデータ構造と大きな違いはないのではないか?と思う人がいるかもしれませんが、この 2 つのデータ構造は、性質が 大きく異なります。
list2 は主に、以下のような 2 種類の用途 で使われます。
- 用途 1
- 同じ種類 の 任意の数のデータ を まとめて扱う
- インデックスは list に代入された複数のデータの 順番 を表す
- for 文などの 繰り返しの処理 の 反復可能オフジェクト として記述することで、先頭の要素から順番 に取り出して 同じ種類の処理を行う ことができる
- 用途 23
- 異なる種類 の 決められた数 のデータを まとめて扱う
- インデックスは、要素に代入されたデータの 種類 を表す
- 一般的 に、for 文などの 繰り返しの処理の反復可能オブジェクトとして 利用しない
修正前 のテストケースを表すデータは、着手を行う座標 という、同じ種類 の 複数の データを要素として持つ list なので、前者の用途 の list です。インデックス は、着手の 順番 を表します。また、テストケースを作成する際に、for 文の中で記述 して利用しています。
修正その 2 のテストケースを表すデータは、着手を行う座標を表すデータ と 期待される処理の値 という、異なる種類 のデータを まとめて扱う ので、後者の用途 の list です。インデックス は、順番ではなく、データの種類 を表します。また、テストケースを作成する際に、テストケースを表すデータを 直接 for 文の中で記述 しません。
修正その 2 の test_judge
の中の for 文の中で記述しているのは、testcase
ではなく、testcase
の 0 番の要素 である、testdata
です。testdata
は 修正前 のテストケースを表すデータなので、前者の用途 の list です。
着手するマスの座標 を表すデータを、x 座標と y 座標の値を要素とする [1, 2]
のような list で表現しましたが、これは 後者の用途 の list です。
修正その 1 のテストケースを表すデータは、前者の用途 の list の要素に、期待される処理の値という、異なる種類のデータ を 追加 してるので、前者の用途 と 後者の用途 が 混在した list になっています。このような list は、インデックスの意味 に データの順番 と データの種類 という 異なる意味が混在 することになってしまうので、扱いづらく、バグの原因 となる 可能性が高く なります。また、pop
メソッドなどを使って、データを分離 しなければ、for 文などの 繰り返しで利用 することは できません。これが、前者の用途 と 前者の用途 を 混在する、修正その 1 のようなデータ構造を 使わないほうが良い 理由です。
引き分けの場合のテスト
引き分け の場合も、同様の方法 でテストケースを用意して、test_judge
を実行します。引き分けの場合は すべてのマス を、誰も勝利しない 状態で埋める必要があるので少し大変ですが、そのような状態である、下図右のテストケースでテストを行うことにします。
下記は、上図左の順番でマークを配置するためのテストケースを表すデータを testcase
に代入し、test_judge
を呼び出すことでテストを行うプログラムです。実行結果から、正しい盤面が作られ、テストで期待される処理が行われることが確認できます。
testcase = [
[
[ 0, 0 ], # 着手順とゲーム盤
[ 0, 1 ], # 136 oox
[ 1, 0 ], # 245 xxo
[ 1, 1 ], # 789 oxo
[ 2, 1 ],
[ 2, 0 ],
[ 0, 2 ],
[ 1, 2 ],
[ 2, 2 ],
],
Marubatsu.DRAW, # 期待される judge メソッドの返り値
]
test_judge(testcase)
実行結果
Turn x
oox
xxo
oxo
ok
複数のテストケースをまとめたデータ構造
test_judge
を使って 3 つのテストケース対してテストを行うためには、下記のプログラムのように、3 種類 のそれぞれのテストケースに対して、「testcase
にテストケースを表すデータを 代入 し、test_judge
を呼び出す」という処理を 3 回記述する 必要があります。このような方法では、テストケースが 増えた場合 にテストを行うのが 大変 です。
testcase = [ 略 ] # 〇 の勝利のデータを記述する
test_judge(testdata)
testcase = [ 略 ] # × の勝利のデータを記述する
test_judge(testdata)
testcase = [ 略 ] # 引き分けのデータを記述する
test_judge(testdata)
そこで、複数のテストケースを 1 つのデータ として まとめ、test_judge
の中で 繰り返し を使って一つ一つのテストケースに対する テストを行う ことにします。
複数 のテストケースを 1 つ の変数で まとめる 方法として、これまでと同様に list を利用 します。下記のプログラムは、3 つ の テストケースを表すデータ を 要素として持つ list を testcases
という名前の変数に代入しています。複数形の名前 にすることで、中に複数 のテストケースのデータが 代入 されていることが 明確になる ようにしています。
testcases = [
# 〇 の勝利のテストケース
[
[
[ 0, 0 ], # 着手順とゲーム盤
[ 0, 1 ], # 135 ooo
[ 1, 0 ], # 24. xx.
[ 1, 1 ], # ... ...
[ 2, 0 ],
],
Marubatsu.CIRCLE, # 期待される judge メソッドの返り値
],
# × の勝利のテストケース
[
[
[ 0, 1 ], # 着手順とゲーム盤
[ 0, 0 ], # 246 xxx
[ 1, 1 ], # 13. oo.
[ 1, 0 ], # 5.. o..
[ 0, 2 ],
[ 2, 0 ],
],
Marubatsu.CROSS, # 期待される judge メソッドの返り値
],
# 引き分けのテストケース
[
[
[ 0, 0 ], # 着手順とゲーム盤
[ 0, 1 ], # 136 oox
[ 1, 0 ], # 245 xxo
[ 1, 1 ], # 789 oxo
[ 2, 1 ],
[ 2, 0 ],
[ 0, 2 ],
[ 1, 2 ],
[ 2, 2 ],
],
Marubatsu.DRAW, # 期待される judge メソッドの返り値
],
]
次に、データ構造の修正に合わせて、test_judge
を以下のように修正します。
-
仮引数の名前 を
testcase
からtestcases
に 修正 する - 2 行目に、
testcases
の先頭から順番に 要素を取り出して、testcase
に代入 する for 文 を記述する - 元の
test_judge
のブロックの内容に インデントを追加 して、2 行目の for 文のブロックの処理 になるように修正する
1 def test_judge(testcases):
2 for testcase in testcases:
3 testdata, winner = testcase
4 mb = Marubatsu()
5 for x, y in testdata:
6 mb.move(x, y)
7 print(mb)
8
9 if mb.judge() == winner:
10 print("ok")
11 else:
12 print("error!")
行番号のないプログラム
def test_judge(testcases):
for testcase in testcases:
testdata, winner = testcase
mb = Marubatsu()
for x, y in testdata:
mb.move(x, y)
print(mb)
if mb.judge() == winner:
print("ok")
else:
print("error!")
修正箇所
インデントの修正部分は省略します。
- def test_judge(testcase):
+ def test_judge(testcases):
+ for testcase in testcases:
testdata, winner = testcase
mb = Marubatsu()
for x, y in testdata:
mb.move(x, y)
print(mb)
if mb.judge() == winner:
print("ok")
else:
print("error!")
下記のプログラムは、testcases
に対して、修正した test_judge
でテストを行っています。実行結果から、3 つのテストケース に対して まとめて 正しく テストが行われる ことが確認できます。
test_judge(testcases)
実行結果
Turn x
ooo
xx.
...
ok
Turn o
xxx
oo.
o..
ok
Turn x
oox
xxo
oxo
ok
これで、命令網羅(C0)のテストが無事完了しました。
分岐網羅(C1) によるテスト
分岐網羅(C1) によるテストは、すべて の条件分岐の 条件式が True
と False
で実行 されるようなテストです。
下記の表は、先程の、命令網羅(C0) によるテストケースに対して、それぞれの条件分岐の条件式がどのような計算結果になるかを表します。
テストケース | 1 つめの条件式 | 2 つめの条件式 | 3 つめの条件式 |
---|---|---|---|
〇 の勝利のテストケース | True |
False |
False |
× の勝利のテストケース | False |
True |
False |
引き分けのテストケース | False |
False |
True |
表から、先程命令網羅(C0) で行ったテストは、分岐網羅(C1) によるテストの条件を満たしている ことがわかるので、新しくテストを行う 必要はありません。また、そのことは、下図の 命令網羅(C0) のフローチャートの図で、すべての線 がいずれかのテストケースで 実行されている ことでも確認することが出来ます。
なお、今回の例では、たまたま 命令網羅(C0) で行ったテストが分岐網羅(C1) によるテストの条件を満たしていますが、分岐網羅(C1) の方が 条件が厳しい ので、一般的 には命令網羅(C0) より 多くのテストケース でテストを行う 必要があります。
条件網羅(C2) によるテスト
以前の記事で説明したように、条件網羅(C2) によるテストは、命令網羅(C0) や 分岐網羅(C1) の条件を 満たさない場合がある ので、個人的 には 中途半端な精度のテスト だと 思います。そのため、本記事では条件網羅(C2) によるテストは行いません。
判定条件/条件網羅(CDC) によるテスト
以前の記事で説明したように、判定条件/条件網羅(CDC) によるテストは、式の値 と、条件式の値 の 因果関係 を 完全にチェックできない 可能性があるという 欠点 があります。そのため、本記事では、判定条件/条件網羅(CDC) によるテストは行いません。
複数条件網羅(MCC) によるテスト
複数条件網羅(MCC) によるテストは、条件分岐の 条件式の中 に、and または or の 論理演算子 が記述されている場合、それらで連結された それぞれの式 が True
と False
の すべての組み合わせ で 1 度は実行 されるようなテストです
judge
メソッドの 1 つ目の条件式 では、下記のプログラムのように、8 つの 式 が or 演算子によって連結されています。このような場合に複数条件網羅(MCC) によるテストを行うと、$2^8 = $ 256 個 ものテストケースを用意する必要があります。これはさすがに 大変すぎる ので、本記事では複数条件網羅(MCC) によるテストは行いません。
# 〇 の勝利の判定
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:
self.winner = Marubatsu.CIRCLE
複雑なデータ構造に対する関数のヒント
テストケースを表すデータのような、複雑なデータ構造 のデータに対する 関数の引数と返り値のデータ型のヒント は 簡潔に記述 することが 難しいの で、そのような場合は 関数のヒント には 何も記述せず、下記のリンク先のように、docstring のほうにデータ構造を 記述 したほうが良いでしょう。
今回の記事のまとめ
記事が長くなったので、今回の記事はここまでにします。
以前の記事 で説明したように、judge
メソッドには いくつかのバグが存在 します。今回の記事では、judge
メソッドに対して、命令網羅(C0) と 分岐網羅(C1) の条件を満たすテストを行いましたが、それらのテストでは バグを発見 することは できませんでした。
これは、judge
メソッドのような、and や or 演算子 を使った複雑な 条件式が記述 されている 関数のテスト を行う場合は、命令網羅(C0) や分岐網羅(C1) のテストでは 不十分 な 場合がある こと原因です。次回以降の記事では、改良条件判断網羅(MC/DC) によるテストなどを使って、バグを発見することが出来ることを示します。
なお、このことから、命令網羅(C0) や分岐網羅(C1) のテストが無意味であるということは 決してありません。例えば、条件式の中で、and や or 演算子が使われていなければ 条件網羅に関するテストを行うことはそもそも 不可能です。
どのテストを行うかは、テストする 関数の性質、求められる テストの精度、費やすことが出来る時間 などから、自分で判断して選択 する必要があるということです。
本記事で入力したプログラム
以下のリンクから、本記事で入力して実行した JupyterLab のファイルを見ることができます。
今回の記事では、marubatsu.py は更新していないので、marubatsu_new.py はありません。
以下のリンクは、今回の記事で更新した test_new.py です。
次回の記事