LoginSignup
0
0

Pythonで〇×ゲームのAIを一から作成する その25 テストを行う関数とデータと処理の分離

Last updated at Posted at 2023-11-05

目次と前回の記事

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

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

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

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

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

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

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

前回までのおさらい

前回の記事では、テストの手法について説明しました。今回の記事では、それらの手法を使って judge メソッドのテストを行います。

python には、unittest や pytest など、テストを行うためのモジュール がいくつかあります(下記のリンク先を参照)。また、VSCode にもテストを行うための機能が用意されています。本記事では、テストで行う 処理の具体的な中身理解してもらいたかった ので、それらのモジュールや機能を 利用せずにテストを行う ことにします。もちろん、それらのモジュールはテストを行う際に便利なので、今回の記事を読んで、テストに興味を持った方は勉強して使ってみて下さい。

judge メソッドのテスト

今回の記事では、judge メソッドのテストの実施方法を、具体例に挙げて説明します。

その際に、データと処理の分離 という、今後のプログラムでも何度も利用する 重要な考え方 について説明します。

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 メソッドのフローチャート

下図は、judge メソッドのフローチャートです。

制御フローテストの一覧

下記の表は、前回の記事で紹介した、制御フローテストの一覧を再掲したものです。

今回の記事では、この中から、命令網羅(C0) の前半を、次回以降の記事で 命令網羅(C0) の後半、分岐網羅(C1)、改良条件判断網羅(MC/DC)、経路組み合わせ網羅judge メソッドのテストを行うことにします。本記事では、他のテストは行いませんが、その理由については記事の中で説明します。

名称 略称 テストケースの数 精度
命令網羅 C0、SC 最小 最小
分岐網羅 C1、DC
条件網羅 C2、CC 小~中
判定条件/条件網羅 CDC 小~中
複数条件網羅 MCC
改良条件判断網羅 MC/DC
経路組み合わせ網羅 なし 最大 最大

命令網羅(C0) によるテスト

記述が長くなるので、以後は テストで利用する「特定のマスにマークが配置された Marubastu クラスのインスタンス」のことを、単に テストケース と表記します。

命令網羅(C0) によるテストを行うためには、すべて条件分岐と文 が 1 度は 実行される ようなテストケースを用意する必要があります。フローチャートでは、すべて の長方形とひし形で表される 図形の処理 が 1 度は 実行される テストケースが必要となります。

judge メソッドには if 文が 3 つあり、いずれも else が記述されていない ので、それぞれの条件文が True になる ようなテストケースを用意する必要があります。先ほどのフローチャートを見て、そのことを確認して下さい。

3 つの if 文の条件式が それぞれ True になる条件 と、その場合に judge メソッドを呼び出した際に 期待される処理 は以下の表のようになります。〇×ゲームの性質上、「〇が勝利した場合」と「×が勝利した場合」と「引き分け」が 同時に起きることは無い ので、3 つ のテストケースが 必要 になることがわかります。

下記の表の期待される処理に None が存在しない ことからわかるように、命令網羅(C0) では、決着がついていない テストケースを 用意する必要がありません。従って、命令網羅(C0) では、決着がついていない状態のテストは 行われません

if 文 条件式が True になる条件 期待される処理(返り値)
1 つ目 〇 が勝利した場合 Marubatsu.CIRCLE
2 つ目 × が勝利した場合 Marubatsu.CROSS
3 つ目 引き分けの場合 Marubatsu.DRAW

プログラミングにある程度慣れている方であれば、上記の説明を見て 違和感 を覚えた人がいるのではないかと思います。実際に、上記の説明の 一部は間違っています が、その点については 勘違いしやすい間違いの実例 として、今後のテストで発見する ことにしますので、間違いがあるまま説明を続行 します。余裕がある方は、何が間違っているかについて考えてみて下さい。

下図は、〇 が勝利した場合、× が勝利した場合、引き分けの場合の 3 つのケースでテストを行った際に行われる処理を、フローチャートで表現した図です。図から、この 3 つのテストケースによって、すべて条件分岐と処理 を表す図形が 実行される ので、命令網羅(C0) の条件を満たす テストが行われることが確認できます。

〇 が勝利した場合のテスト

まず、〇 が勝利した場合 のテストケースを考えます。命令網羅(C0) では、〇 が勝利 した場合であれば、どのようなゲーム盤でも構わない ので、本記事では 0 行に 〇 が 3 つ並んだ ゲーム盤を表すテストケースを用意することにします。もちろん、〇が勝利した場合の 他のテストケース を用意しても 構いません

前々回の記事 では、judge メソッドの処理を確認する際に、place_mark メソッドを使って、ゲーム盤に 〇 だけを配置 した状態で確認を行いました。そのような、実際の 〇×ゲームを遊ぶ 手順と異なる方法 でマークを配置するようなテストケースは、手抜き を行っているので 用意するのが簡単 である 反面実際 の 〇×ゲームでは 起きない状況 に対するテストになるので、テストの 精度が低くなる という問題があります。

そこで、今回は、着手 を行う move メソッドを使って、実際に 〇×ゲームを 遊ぶ場合と同じ の手順で、〇 と × を交互に配置 することで、0 行に 〇 が 3 つ並んだゲーム盤を表すテストケースを用意することにします。

下記のプログラムは、下図の 左の順番着手 を行うことで、下図の 右の 〇 が勝利した場合 のテストケースを mb1 に代入しています。

テストケースでマークが 正しく配置 されているかどうかを 確認 するために、9 行目でゲーム盤を表示しています。テストケースが 間違っている 場合は、正しいテスト を行うことが 出来ない ので、テストケースの確認 も、テストにおける 重要な作業 です。

実行結果から、正しいマスにマークが配置されていることが確認できます。

1  from marubatsu import Marubatsu
2
3  mb1 = Marubatsu()
4  mb1.move(0, 0)
5  mb1.move(0, 1)
6  mb1.move(1, 0)
7  mb1.move(1, 1)
8  mb1.move(2, 0)
9  print(mb1)
行番号のないプログラム
from marubatsu import Marubatsu

mb1 = Marubatsu()
mb1.move(0, 0)
mb1.move(0, 1)
mb1.move(1, 0)
mb1.move(1, 1)
mb1.move(2, 0)
print(mb1)

実行結果

Turn x
ooo
xx.
...

下記は、このテストケースを使って judge メソッドのテストを行うプログラムです。1 行目の if 文の 条件式で judge メソッドを呼び出し返り値〇 の勝利 を表すMarubatsu.CIRCLE であれば 2 行目で "ok" を、そうでなければ 4 行目で "error!" を表示しています。実行結果から、この テストケースに対して judge メソッドが 期待される処理を行う ことを 確認 することが出来ます。

if mb1.judge() == Marubatsu.CIRCLE:
    print("ok")
else:
    print("error!")

実行結果

ok

前回の記事 で、期待される処理が 行われていた場合何も表示しない 場合が多いと説明しましたが、今回は 1 つのテストケースに対してのみテストを行っているので 大量の表示行われることは無い 点と、何も表示しないと 何が起きたかがわかりづらい ので、"ok" を表示することにしました。

テストを行うための関数の定義

次に、× の勝利の場合のテストを行う必要がありますが、テストを行う際に、毎回 上記のような 長いプログラムを記述 するのは 大変 です。そこで、今後のテストを 効率よく行う ことが出来るようにするために、テストを行うための関数を定義 することにします。

関数の仕様

テストを行うための関数を定義するためには、関数の名前入力処理出力仕様を決める 必要があり、本記事では、それぞれを以下のように決めることにします。

  • 関数の名前judge メソッドのテスト(test)を行うので、test_judge とする

  • 入力:テストを行うためには、テストケースを表すデータ が必要なので、仮引数 testcase にそのデータを代入することにする

  • 処理testcase に代入されたデータを使って テストを行い、その 結果を表示 する

  • 出力:上記の処理によって、画面に結果が表示 されるので、関数の返り値としての出力は 必要がない。従って、返り値を返さない関数 とする

具体的には、下記のような関数を作成することにします。この関数のブロックにどのようなプログラムを記述すれば良いかについて少し考えてみて下さい。

def test_judge(testcase):
    テストケースを使ってテストを行い結果を表示する

データと処理の分離

test_judge を、下記のようなプログラムで定義する事を思い浮かべた人が多いのではないでしょうか?下記のプログラムは、テストケースを直接 test_judge の仮引数に 代入 してテストを行う関数です。

この関数のブロックの中のプログラムは、先ほどのテストを行う処理 の、mb1.judge() == Marubatsu.CIRCLE を、testcase.judge() == Marubatsu.CIRCLE に修正したものです。

def test_judge(testcase):
    if testcase.judge() == Marubatsu.CIRCLE:
        print("ok")
    else:
        print("error!")    
修正箇所

インデントは修正箇所に含みません。

def test_judge(testcase):
-   if mb1.judge() == Marubatsu.CIRCLE:
+   if testcase.judge() == Marubatsu.CIRCLE:
        print("ok")
    else:
        print("error!")    

この test_judge を使って、下記のプログラムのように、実際 に 0 行に 〇 が 3 つ並んだテストケースの テストを行う ことができます。

mb1 = Marubatsu()
mb1.move(0, 0)
mb1.move(0, 1)
mb1.move(1, 0)
mb1.move(1, 1)
mb1.move(2, 0)

test_judge(mb1)

実行結果

ok

上記の test_judge を利用することで、テストケースごとに、テストを行う処理 を毎回 記述する 必要は なくなります が、あいかわらず、特定のマスにマークが配置 されたテストケースを 作成する ための プログラムを記述 する 必要がある 点が 改善されていません

この問題の原因は、テストケースを作成するための プログラムの中 に、直接 マークを配置するために必要な データが記述 されている点です。プログラムに記述された データ処理分離 することで、この問題を 改善する ことが出来ます。

具体的には、テストケースを作成 するために 必要なデータ を記述し、test_judge の中で、そのデータを使って テストケースを 作成する処理 を記述します。そのためには、テストケースを 作成 するために 必要なデータデータ構造決める 必要があります。

表記が長いので、以後は「テストケースを作成するために必要なデータ」のことを、テストケースを表すデータ のように表記します。

言葉の説明だけではわかりづらいと思いますので、具体例を挙げて説明します。

データと処理を分離することの利点

先程は、下記のようなプログラムを記述して、0 行に 3 つの 〇 が配置された Marubatsu クラスのインスタンスをテストケースとして作成しました。

1  mb1 = Marubatsu()
2  mb1.move(0, 0)
3  mb1.move(0, 1)
4  mb1.move(1, 0)
5  mb1.move(1, 1)
6  mb1.move(2, 0)
行番号のないプログラム
mb1 = Marubatsu()
mb1.move(0, 0)
mb1.move(0, 1)
mb1.move(1, 0)
mb1.move(1, 1)
mb1.move(2, 0)

上記のプログラムの 2 ~ 6 行目の move メソッドの 実引数 として記述された数値が、0 行 に 3 つの 〇 が配置された テストケースを表すデータ です。上記のプログラムから、テストケースを表すデータを 分離 するために、move メソッドに記述する 実引数を表すデータ を、変数に代入して記述する ことにします。

move メソッドで着手を行う際に 必要となるデータ は、マスの x, y 座標 です。そのような 2 つのデータは、list、tuple、dict などを使って表現できますが、簡潔に表現 するのであれば、list か tuple を使うと良いでしょう。本記事では list を使って、下記のプログラムのように、マスの x 座標マスの y 座標 の順番でマスの座標を表すデータを 5 つの変数 で記述することにします。tuple や dict を使いたい人は変更してもかまいません。

data1 = [ 0, 0 ]
data2 = [ 0, 1 ]
data3 = [ 1, 0 ]
data4 = [ 1, 1 ]
data5 = [ 2, 0 ]

tuple を利用する場合は、(0, 0)、dict を利用する場合は、{ "x": 0, "y": 0 } のようにデータを記述します。dict を使うと 記述が長くなる という 欠点 がありますが、キー"x" のような 文字列を使う ことで、list や tuple の場合と比べてdict の 値の意味がわかりやすくなる という利点があります。dict を使った具体例については、次回以降の記事で紹介します。

テストケースを表すデータを分離することが出来たので、次は、分離したデータ を使って、テストケースを 作成する処理記述 することにします。

着手するマスの x 座標 は、list の 0 番 の要素y 座標 は list の 1 番の座標 に代入されているので、5 つのマスに着手するプログラムを下記のように記述することが出来ます。

実行結果から、正しいマスにマークが配置されていることが確認できます。

1  mb1 = Marubatsu()
2  mb1.move(data1[0], data1[1])
3  mb1.move(data2[0], data2[1])
4  mb1.move(data3[0], data3[1])
5  mb1.move(data4[0], data4[1])
6  mb1.move(data5[0], data5[1])
7  print(mb1)
行番号のないプログラム
mb1 = Marubatsu()
mb1.move(data1[0], data1[1])
mb1.move(data2[0], data2[1])
mb1.move(data3[0], data3[1])
mb1.move(data4[0], data4[1])
mb1.move(data5[0], data5[1])
print(mb1)

実行結果

Turn x
ooo
xx.
...

元のプログラムでは、テストケースを表すデータ がプログラム内に 直接記述 されていたため、常に 0 行に 〇 が 3 つ配置されたテストケースが作成されることになります。

一方、上記のプログラムには、具体的なデータ記述されていない ので、下記のプログラムのように、data1 などに代入する 値を変える ことで、同一のプログラムを使って、異なる テストケースを 作成 することが出来るようになります。

下記のプログラムでは、2 行 に 〇 が 3 つ配置されたテストケースを作成するプログラムですが、先程と比べて、1、3、5 行目の テストケースを表すデータ変化 していますが、9 ~ 14 行目の インスタンスを作成する処理 の部分は 全く 変化 していません

 1  # data1、data3、data5 が変化している
 2  data1 = [ 0, 2 ] 
 3  data2 = [ 0, 1 ]
 4  data3 = [ 1, 2 ]
 5  data4 = [ 1, 1 ]
 6  data5 = [ 2, 2 ]
 7
 8  # テストケースを作成する処理は先ほどと全く同じ
 9  mb1 = Marubatsu()
10  mb1.move(data1[0], data1[1])
11  mb1.move(data2[0], data2[1])
12  mb1.move(data3[0], data3[1])
13  mb1.move(data4[0], data4[1])
14  mb1.move(data5[0], data5[1])
15  print(mb1)
行番号のないプログラム
# data1、data3、data5 が変化している
data1 = [ 0, 2 ] 
data2 = [ 0, 1 ]
data3 = [ 1, 2 ]
data4 = [ 1, 1 ]
data5 = [ 2, 2 ]

# テストケースを作成する処理は先ほどと同じ
mb1 = Marubatsu()
mb1.move(data1[0], data1[1])
mb1.move(data2[0], data2[1])
mb1.move(data3[0], data3[1])
mb1.move(data4[0], data4[1])
mb1.move(data5[0], data5[1])
print(mb1)
修正箇所
- data1 = [ 0, 0 ] 
+ data1 = [ 0, 2 ] 
data2 = [ 0, 1 ]
- data3 = [ 1, 0 ]
+ data3 = [ 1, 2 ]
data4 = [ 1, 1 ]
- data5 = [ 2, 0 ]
+ data5 = [ 2, 2 ]

mb1 = Marubatsu()
mb1.move(data1[0], data1[1])
mb1.move(data2[0], data2[1])
mb1.move(data3[0], data3[1])
mb1.move(data4[0], data4[1])
mb1.move(data5[0], data5[1])
print(mb1)

実行結果

Turn x
...
xx.
ooo

データと処理を分離 することで、分離したデータ内容を変化 させることで、同じプログラム異なる テストケースを 作成できる ようになります。

この話を聞いた人は、関数 や、繰り返し などでも 同様データと処理の分離 を行っているのではないかと思った方がいるかもしれませんが、その通り です。関数の場合は、仮引数データ関数のブロック処理 という形でデータと処理の分離を行っています。データと処理を分離するという 考え方 は、過去の記事で既に 紹介済 ということです。

実際に、この後で、テストケースを表すデータtest_judge仮引数に代入 し、test_judgeブロックの中 で、そのデータを使ってテストケースを 作成する処理を記述 するようにプログラムを修正します。

テストケースを表すデータのデータ構造

先程の、テストケースを表すデータを、5 つの変数 に分けて 代入する というデータ構造には、5 つ の着手を行う座標を表す データしか表現できない という欠点があります。また、着手を表す データごと1 つの変数を用意 して代入するのは 面倒 です。

着手 行う座標を表す 複数のデータ は、list や tuple を使うことで 1 つ の変数に まとめて代入 することが出来ます。list と tuple の どちらを使っても構いません が、先程は 2 つの座標を表すデータを list で表現したので、今回も list を使うことにします

下記のプログラムは、5 つ の着手を行う座標を表す データ を、1 つの listまとめた ものを、testcase という変数に代入するプログラムです。

data1 = [ 0, 0 ]
data2 = [ 0, 1 ]
data3 = [ 1, 0 ]
data4 = [ 1, 1 ]
data5 = [ 2, 0 ]
testcase = [ data1, data2, data3, data4, data5 ]

上記のプログラムは、5 つの座標を表すデータを、一旦変数に代入していましたが、下記のプログラムのように、それぞれのデータを 直接 list の要素として記述する ことで、1 つの変数だけ を使って データを代入 するが出来ます。また、testcase に代入されるデータは、2 次元配列 を表す list になります。

なお、2 次元配列を表す list は 意味が分かりづらい ので、下記のプログラムでは、コメントtestcaseどのようなデータ を表すかを 示す という 工夫 を行っています。

testcase = [
    [ 0, 0 ], # 着手順とゲーム盤
    [ 0, 1 ], # 135  ooo
    [ 1, 0 ], # 24.  xx.
    [ 1, 1 ], # ...  x..
    [ 2, 0 ],
]

どこかで聞いたような話だと思った方がいるかもしれませんが、実際に同様の説明を 過去の記事 で行っています。かなり前の記事だったので、もう一度同じ説明をしましたが、以後は このような場合の 説明を省略 することにします。

この testcase を使って 5 つのマスに着手を行うプログラムは以下のようになります。意味が分かりづらいと思った方は、testcase[0] には data1同じデータが代入 されているとを思い出してください。従って、testcase[0][0]data1[0] には 同じデータが代入 されています。下記の修正箇所を見て、そのことを確認して下さい。

実行結果から、正しいマスにマークが配置されていることが確認できます。

mb1 = Marubatsu()
mb1.move(testcase[0][0], testcase[0][1])
mb1.move(testcase[1][0], testcase[1][1])
mb1.move(testcase[2][0], testcase[2][1])
mb1.move(testcase[3][0], testcase[3][1])
mb1.move(testcase[4][0], testcase[4][1])
print(mb1)
修正箇所
mb1 = Marubatsu()
- mb1.move(testcase[0][0], testcase[0][1])
+ mb1.move(data1[0], data1[1])
- mb1.move(testcase[1][0], testcase[1][1])
+ mb1.move(data2[0], data2[1])
- mb1.move(testcase[2][0], testcase[2][1])
+ mb1.move(data3[0], data3[1])
- mb1.move(testcase[3][0], testcase[3][1])
+ mb1.move(data4[0], data4[1])
- mb1.move(testcase[4][0], testcase[4][1])
+ mb1.move(data5[0], data5[1])
print(mb1)

実行結果

Turn x
ooo
xx.
...

これで、0 行に 3 つの 〇 が配置されたテストケースを表すデータを記述して、testcase という 1 つの変数代入 することができました。

テストケースを表すデータデータ構造 は、以下のようになります。

  • 着手する マスの座標 を表すデータを 要素 とする list で表現 する
  • list の 要素の順番 は、着手 を行う 順番に対応 する
  • マスの座標 は、マスの x 座標y 座標要素 とする list で表現 する

上記のように、テストケースを表すデータは、list の要素の中に list が 入れ子で代入 されるので 2 次元配列 を表す list になります。また、1 つ目 のインデックスが 着手の順番 を、2 つ目 のインデックスが x 座標 または y 座標 を表す 数値 になります。

テストケースを表すデータを使ってテストケースを作成するプログラム

先程の testcase のデータからテストケースを作成する下記のプログラムは、5 つ の着手を行うプログラムですが、testcase のデータ内に記録された 着手の数5 つ である とは限りません。従って、下記のプログラムは、5 つ以外 の着手が 記録 された testcase のデータに対して 利用する ことは できません。そのため、任意の数 の着手が記録された testcase にからテストケースを作成するプログラムを記述する必要があります。

mb1 = Marubatsu()
mb1.move(testcase[0][0], testcase[0][1])
mb1.move(testcase[1][0], testcase[1][1])
mb1.move(testcase[2][0], testcase[2][1])
mb1.move(testcase[3][0], testcase[3][1])
mb1.move(testcase[4][0], testcase[4][1])
print(mb1)

testcase を使ってテストケースを作成するプログラムは、testcast に代入された list先頭の要素 が表す座標 から順番 に着手を行う処理を行います。従って、下記のプログラムのように、for 文 を使った 繰り返しで 記述することができます。

2 行目の for 文によって、繰り返しのたびに testcase に代入された list の先頭の要素 から 順番に data という 変数に代入 されて、3 行目の for 文の ブロックの処理 が行われます。data に代入された list の要素 には、着手を行うマスの x 座標と y 座標 を表すデータが代入されているので、3 行目のように記述することで、testcase に記録された 着手を行うマスの座標 を表すデータを 先頭から順番 に使って 着手 が行われます。

実行結果から、正しいマスにマークが配置されていることが確認できます。

mb1 = Marubatsu()
for data in testcase:
    mb1.move(data[0], data[1])
print(mb1)

実行結果

Turn x
ooo
xx.
...

プログラムをわかりやすくする工夫

上記のプログラムの 3 行目の move メソッドの実引数に記述した data[0]data[1] は、それらが具体的に何のデータを表しているかが わかりづらい という欠点があります。そこで、それらを わかりやすい名前の変数に代入 することでプログラムを わかりやすくする という 工夫 を行うことにします。このような修正を行うのは、プログラムを わかりやすく記述する ことで、プログラムの バグの発生を減らしたり、バグが発生した場合でも バグの原因を見つけやすくなる という 利点 が得られるかです。

data[0]data[1] は、それぞれ x 座標y 座標 を表すので、下記のプログラムのように、xy という変数にそれぞれの値を代入するように修正します。修正前のプログラムと見比べて、見た目が わかりやすくなっている ことを確認して下さい。

実行結果から、正しいマスにマークが配置されていることが確認できます。

mb1 = Marubatsu()
for data in testcase:
    x = data[0]
    y = data[1]
    mb1.move(x, y)
print(mb1)
修正箇所
mb1 = Marubatsu()
for data in testcase:
+   x = data[0]
+   y = data[1]
-   mb1.move(data[0], data[1])
+   mb1.move(x, y)
print(mb1)

実行結果

Turn x
ooo
xx.
...

以前の記事 で、tuple の要素を 個別の変数に代入する 処理を紹介しましたが、list の要素 に対しても 3 行目のように、同様の処理を記述 することができます。下記のプログラムは、先程のプログラムの3、4 行目の処理を、1 行のプログラムでまとめたものです。

実行結果から、正しいマスにマークが配置されていることが確認できます。

mb1 = Marubatsu()
for data in testcase:
    x, y = data
    mb1.move(x, y)
print(mb1)
修正箇所
mb1 = Marubatsu()
for data in testcase:
-   x = data[0]
-   y = data[1]
+    x, y = data
    mb1.move(x, y)
print(mb1)

実行結果

Turn x
ooo
xx.
...

上記のプログラムは、for 文の 変数の所x, y直接記述 することで、下記のプログラムのように、さらに簡潔に記述 することが出来ます。

実行結果から正しいマスにマークが配置されていることが確認できます。

mb1 = Marubatsu()
for x, y in testcase:
    mb1.move(x, y)
print(mb1)
修正箇所
mb1 = Marubatsu()
- for data in testcase:
+ for x, y in testcase:
-   x, y = data
    mb1.move(x, y)
print(mb1)

実行結果

Turn x
ooo
xx.
...

テストを行う関数の定義

マークをまとめて配置する処理が記述できたので、下記のプログラムのように、先程記述した、テストケースのテストを行うプログラムを後ろに記述して 組み合わせます

実行結果から正しく動作することが確認できます。

testcase = [
    [ 0, 0 ], # 着手順とゲーム盤
    [ 0, 1 ], # 135  ooo
    [ 1, 0 ], # 24.  xx.
    [ 1, 1 ], # ...  ...
    [ 2, 0 ],
]

mb1 = Marubatsu()
for x, y in testcase:
    mb1.move(x, y)
print(mb1)

if mb1.judge() == Marubatsu.CIRCLE:
    print("ok")
else:
    print("error!")

実行結果

Turn x
ooo
xx.
...

ok

テストケースを表すデータ に対して テストを行う 処理が記述できたので、この処理を行う test_judge という関数を下記のプログラムのように定義する事にします。この関数のブロックの中のプログラムは、関数にする前のプログラムと 基本的には同じ ですが、関数の中で Marubatsu クラスの インスタンスは 1 つしか存在しない ので、mb1 という名前を mb という名前に変更した点が異なります。また、最初に定義した test_judge仮引数 testcase に代入する値は、テストケースそのものでしたが、下記の test_judge の場合は、テストケースを表すデータ である点に注意して下さい。

 1  def test_judge(testcase):
 2      mb = Marubatsu()
 3      for x, y in testcase:
 4         mb.move(x, y)
 5      print(mb)
 6
 7      if mb.judge() == Marubatsu.CIRCLE:
 8          print("ok")
 9      else:
10          print("error!")
行番号のないプログラム
def test_judge(testcase):
    mb = Marubatsu()
    for x, y in testcase:
        mb.move(x, y)
    print(mb)

    if mb.judge() == Marubatsu.CIRCLE:
        print("ok")
    else:
        print("error!")
修正箇所
def test_judge(testcase):
-   mb1 = Marubatsu()
+   mb = Marubatsu()
    for x, y in testcase:
-       mb1.move(x, y)
+       mb.move(x, y)
-   print(mb1)
+   print(mb)

-   if mb1.judge() == Marubatsu.CIRCLE:
+   if mb.judge() == Marubatsu.CIRCLE:
        print("ok")
    else:
        print("error!")

下記のプログラムのように、test_judge の実引数に testcase を記述して呼び出すことで、テストを正しく行えることが確認できます。

test_judge(testcase)

実行結果

Turn x
ooo
xx.
...

ok

次回の記事について

記事が長くなったので今回はここまでにします。

今回の記事で作成した test_judge はまだ 未完成 なので、このままでは命令網羅(C0) の残りのテストケースのテストで使うことはできません。次回の記事ではこの関数を完成させ、命令網羅(C0) のテストを完了することにします。

テストのプログラムのモジュールについて

テストで使用する関数は、〇× ゲームのプログラムとは 直接関係がない プログラムなので、test.py というファイルに保存することにします。なお、test.py の中に記述する test_judge は、marubatsu モジュールで定義された Marubatsu クラスを利用するので、先頭に from marubatsu import Marubatsu を記述する必要があります。

test.py の内容は、marubatsu.py と同様に、本記事で入力したプログラムの中のリンクから参照できるようにします。

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

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

今回の記事では、marubatsu.py は更新していないので、marubatsu_new.py はありません。

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

次回の記事

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