Pythonで〇×ゲームのAIを一から作成する　その32　× の勝利の判定に対するテストと test_judge の改良

# 目次と前回の記事

https://qiita.com/ysgeso/items/2381dd4e3283cbed49a0

https://qiita.com/ysgeso/items/ea147c436e357281f441

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

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

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

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

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

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

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

https://github.com/ysgeso/marubatsu/blob/master/32/marubatsu.py

https://github.com/ysgeso/marubatsu/blob/master/032/test.py

## 前回までのおさらい

前回の記事では、`Marubatsu` クラスの `judge` メソッドに対して、[改良条件判断網羅](https://qiita.com/ysgeso/items/728cee8982fe0ed9da38#%E6%94%B9%E8%89%AF%E6%9D%A1%E4%BB%B6%E5%88%A4%E6%96%AD%E7%B6%B2%E7%BE%85modified-condition--dicision-coverage)（以後は MC/DC と表記します）でのテストを開始しました。具体的には、〇 の勝利の判定を行う if 文に対するテストケースを作成し、テストを行いました。

今回の記事ではその続きを行います。

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

前回の記事で、〇 の勝利の判定を行う if 文に対するテストを行ったので、次は、__× の勝利の判定__ を行う if 文に対するテストを行います。なお、表記が長いので、以降は「× の勝利の判定を行う if 文に対するテストケース」を、__× の勝利のテストケース__ のように表記します。

# × が勝利した場合のテストケース

下記は、`judge` メソッドの中で、× の勝利を判定する if 文です。

```python
        # × の勝利の判定
        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[0][2] == Marubatsu.CROSS:
            winner = Marubatsu.CROSS   
```

このプログラムは、__〇 の勝利を判定__ する if 文と __同様__ に、条件式は、__or 演算子のみ__ で __連結__ されています。従って、この if 文に対する MC/DC のテストケースは、〇 の勝利のテストケースと __同様__ に、__下記の性質を満たす__ ようなテストケースを用意すれば良いことがわかります。

* __or 演算子__ で __連結__ された __式__ の計算結果が __すべて `False` になる__ テストケース
* __or 演算子__ で __連結__ された __式__ の計算結果のうち、__1 つだけ__ が __`True` になる__ テストケース

## すべてが `False` になるテストケース

上記のうち、__すべてが `False`__ になるテストケースは、__〇 の勝利のテストケース__ として用意した、ゲーム盤に一つも __マークが配置されていない__ テストケースを __そのまま利用__ することができます。従って、すべてが `False` になるテストケースを __新しく作成__ する __必要はありません__。

## 1 つだけが `True` になるテストケース

× の勝利を判定する if 文の条件式の中の、or 演算子で連結された __それぞれ__ の __式が `True`__ になるのは、__下図__ のマスに __× が配置された場合__ です。下図の __8 通り__ の __いずれの場合__ でも、__同時__ に __2 つ以上__ の場所で × が __3 つ並ぶことはない__ ので、__1 つだけ__ が __`True` になる__ ことが __保証__ されます。従って、__下図__ のように __× が配置__ された、__8 つ__ のテストケースを __用意すれば良い__ ことがわかります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/25593bad-5fc3-b033-19c6-6d8e4312b814.png" width="250">

次に、その __8 つのテストケース__ に対する __`judge` メソッド__ の __期待される返り値__ について考える必要があります。ここまでの説明は、[前回の記事](https://qiita.com/ysgeso/items/ea147c436e357281f441#1-%E3%81%A4%E3%81%A0%E3%81%91%E3%81%8C-true-%E3%81%AB%E3%81%AA%E3%82%8B%E3%83%86%E3%82%B9%E3%83%88%E3%82%B1%E3%83%BC%E3%82%B9) の、__〇 が勝利した場合__ と __同様__ なので、この後の説明も同じような説明になると思う人がいるかもしれませんが、__ここから__ の説明は __異なります__。

__× を 3 つ配置__ するということは、__その前__ に __〇 を 3 回配置__ することになります。従って、下図のように、__〇 が 3 つ並んで配置__ するようなテストケースを作成しても、× の勝利を判定する if 文の条件式の __1 つだけ__ が __`True` になる__ ことに変わりはありません。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/6d52d76d-c7e3-575a-1f61-8b94cefbefc0.png" width="100">

ただし、下記の 〇×ゲームの __仕様 6__ で、__同じマークが 3 つ並んだ場合__ は、__ゲームが終了__ することが決められています。従って、上記のような __〇 と ×__  が __同時に 3 つ並ぶ__ ような __状況__ が起きることは __あり得ない__ ことがわかります。

「__プレイヤーがマークを置いた結果、縦、横、斜めのいずれかの一直線の 3 マスに同じマークが並んだ場合、そのマークのプレイヤーの勝利とし、ゲームが終了する__」

__実際__ の 〇×ゲームで __出現しない__ ようなゲーム盤の状況を表す __テストケース__ を作成してテストを行っても __意味はない__ ので、× の勝利のテストケースは、__〇 が勝利していない状態__ で __作成する必要__ があります。

× の勝利のテストケースでは、__着手__ は __〇 の 3 回__、__× の 3 回__ の、__計 6 回__ になるので、__引き分け__ になることは __ありません__。従って、__引き分けを判定__ する if 文の __条件式__ の計算結果も __必ず `False`__ になります。

下図は、上記の __条件を満たす__ テストケースに対する __`judge` メソッド__ の __処理の流れ__ を表す __フローチャート__ で、__赤い枠線と線__ が行われる __処理の流れ__ を表します。先ほど説明したように、用意するテストケースは、__〇 が勝利しない__ ように作られるので、__〇 の勝利を判定__ する if 文の条件式の計算結果は __常に `False`__ になります。図から、__`winner`__ に関する __代入処理__ は、__赤字__ の __`winner = Marubatsu.CROSS`__ が __最後に行われる__ ので、`judge` メソッドの __期待される返り値__ は __`Marubatsu.CROSS`__ であることがわかります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/f02b51ca-cc30-5e6c-3b48-f8eb3e5544a0.png" width="500">

下記は、上記の条件を満たす 8 つの テストケースを記述したプログラムです。なお、前回の記事で記述した、__〇 の勝利__ のテストケースを __記述しない理由__ は __後述__ します。

```python
from marubatsu import Marubatsu

testcases = {
    # × の勝利のテストケース
    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",
    ],
}
```

次に、このテストケースに対して __`test_judge` を実行__ してテストを行います。まず、__実行結果を見て__、__正しいマス__ にマークが __配置__ されていることを __必ず確認__ して下さい。


```python
from test import test_judge

test_judge(testcases)
```

実行結果
```
Turn o
xxx
oo.
o..

ok
Turn o
oo.
xxx
o..

ok
Turn o
oo.
o..
xxx

ok
Turn o
xoo
xo.
x..

ok
Turn o
oxo
ox.
.x.

ok
Turn o
oox
o.x
..x

ok
Turn o
xo.
ox.
o.x

ok
Turn o
oox
ox.
x..

test_judge error!
mb.judge(): playing
winner:     x
```

実行結果に __表示される__、8 つのテストケースによって着手が行われた __ゲーム盤__ から、__正しいマス__ にマークが __配置__ されていることが __確認__ できます。

次に、__`judge` メソッド__ が、__期待される仮り値__ を返しているかどうかを __確認__ します。下記の __実行結果__ の __最後の部分__ から、__最後のテストケース__ に対して、__`mb.judge()` の返り値__ が、__期待される返り値__ を表す `winner` に代入された __`"x"`__ とは __異なる__、__`"playing"`__ になっていることがわかります。

```
略
Turn o
oox
ox.
x..

test_judge error!
mb.judge(): playing
winner:     x
```

このエラーメッセージが表示されるのは、`mb.judge()` の返り値と `winner` に代入された値の __どちらか__、または __両方__ が __間違っている__ ことが原因なので、その点について検証します。

このテストケースは、(2, 0)、(1, 1)、(0, 2) のマスに × が並んでいるため、__× が勝利__ しています。従って、期待される返り値を表す __`winner` が `"x"` である__ ことは __正しい__ です。従って、__`mb.judge()` の返り値__ が、__ゲームが決着していない__ ことを表す __`"playing"`__ になっている点が __間違っています__。

このことから __`judge` メソッド__ の、__× の勝利を判定__ する if 文の条件式の中で、__(2, 0)、(1, 1)、(0, 2)__ のマスに __× が配置される__ ことを判定する部分にバグが存在する __可能性が高い__ ことがわかります。

`judge` メソッドの __その部分の条件式__ を調べてみると、下記のプログラムのように、(2, 0)、(1, 1）、(2, 2) という __間違ったマス__ を __判定__ していることがわかります。

```python
        self.board[2][0] == self.board[1][1] == self.board[2][2] == Marubatsu.CROSS:
```

従って、この部分を下記のプログラムのように、__正しいマスを調べる__ ように __修正__ することでこの __バグを修正__ することができます。

```python
        self.board[2][0] == self.board[1][1] == self.board[0][2] == Marubatsu.CROSS:
```

<details><summary><strong>修正箇所</strong></summary>

```diff_python
-       self.board[2][0] == self.board[1][1] == self.board[2][2] == Marubatsu.CROSS:
+       self.board[2][0] == self.board[1][1] == self.board[0][2] == Marubatsu.CROSS:
```
</details>

上記以外の部分は修正していないので、修正した `judge` メソッドの全体をこの記事に記述しませんが（github のほうには記述して実行します）、修正後に下記のプログラムを実行することで、最後のテストケースで __エラーメッセージ__ が __表示されなくなり__、`judge` メソッドの __バグが修正__ されたことが __確認__ できます。なお、実行結果は、最後の部分のみを記述します。

```python
先程と同じなので省略

Turn o
oox
ox.
x..

ok
```

これが、`judge` メソッドの、3 つあるバグの中の __1 つ目のバグ__ です。このバグのような、__データの入力ミス__ が __原因__ となるバグは __よくある事__ です。特に、`judge` メソッドのように、__大量のデータを入力__ する必要があるプログラムでは、__頻発する__ ので __注意が必要__ です。

このように、__MC/DC テスト__ を行うことで、`judge` メソッドに __バグ__ が存在することを __発見__ し、エラーメッセージからその __原因を特定__ して __修正__ することができました。

# `test_judge` の改良

上記で、× の勝利を判定する if 文のテストケースの作成が終了しましたが、現状の __`test_judge`__ には __いくつかの問題__ があるので、次に行く前に __改良__ を行うことにします。

## × の勝利のテストケースのみを記述した理由

先程、__× の勝利__ のテストケースを __記述__ する際に、前回の記事で作成した __〇 の勝利__ のテストケースを __記述しなかった理由__ について説明します。

下記は、__〇 の勝利__ のテストケースを __含めた__ プログラムです。

```python
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",
    ],
    # × の勝利のテストケース
    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",
    ],
}
```

このテストケースに対して __`test_judge`__ で __テストを行う__ と、下記の実行結果のように、__すべてのテストケース__ に対する __ゲーム盤の表示__ が行われるため、__非常に長い表示__ が行われてしまします。

```python
test_judge(testcases)
```

<details><summary><strong>実行結果</strong>（長いので、クリックすると表示するようにしました）</summary>

```
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
Turn o
xxx
oo.
o..

ok
Turn o
oo.
xxx
o..

ok
Turn o
oo.
o..
xxx

ok
Turn o
xoo
xo.
x..

ok
Turn o
oxo
ox.
.x.

ok
Turn o
oox
o.x
..x

ok
Turn o
xo.
ox.
o.x

ok
Turn o
oox
ox.
x..

ok
```
</details>

:::note
JupyterLab では、__一定以上の行数__ の長い __表示__ を行うようなプログラムを実行した場合に、__表示の一部__ のみが表示され、__その後__ で __下記のような表示__ が行われる場合があります。

```
Output is truncated. View as a scrollable element or open in a text editor. Adjust cell output settings...
```

最初の文は、__出力__（output）（表示の事）が __途中__ で __切り捨てられている__（truncated）という意味です。その後の __`scrollable element`__ の部分は __リンク__ になっており、__クリック__ することで、__スクロール可能__ （scrollable）な __セル__ に __表示が行われる__ ようになります。

また、その後の __`text editor`__ のリンクを __クリック__ することで、VSCode の __新しいタブ__ に、（文字のみを扱う）__テキストエディタ__ として __結果が表示__ されるようになります。

プログラムの実行結果を、__切り捨てず__ に __何行まで表示__ するかは、VSCode の __設定で変更__ することができます。最後に表示される __`settings`__ のリンクを __クリック__ することで、その設定を行う画面が　VSCode の __新しいタブ__ に表示されます。変更したい人は、__Notebook Output Text Limit__ の項目が表示する行数を表すので、その部分を修正して下さい。
:::

__× の勝利__ のテストケースを __新しく記述__ して __テストを行う__ 際に、__上記__ のような __実行結果が表示__ された場合の事を __考えてみて下さい__。上記の実行結果で表示されたゲーム盤のうち、__新しく追加__ した __× の勝利__ のテストケース __以外__ の __ゲーム盤の表示__ は、__既に__ 前回の記事で  __マークの配置__ を __確認済__ なので、__表示__ する __必要はありません__。

先程、__× の勝利__ のテストケース __のみを記述__ してテストを行った理由は、__新しく記述__ したテストケースに対して __のみ__、__正しいマス__ に __マークが配置__ されていることを __確認__ するためです。

テストケースを __新しく作成__ した場合は、その __テストケースだけ__ で、__`test_judge` を実行__ して、テストケースによって __正しいマスにマークが配置__ されることを __確認__ すると良いでしょう。

## テストケースの管理方法

上記の説明を読んだ人で、テストケースを追加する際に、__全て__ のテストケースを __まとめて記述__ するの __ではなく__、テストケースを __追加するたび__ に、__異なる変数__ を用意して、その変数に __追加する__ テストケースを記述したものを __代入__ したほうが良いのではないかと思った人はいないでしょうか？

もちろん、__複数の変数__ でテストケースを __管理__ してテストを行うことは可能ですが、そのような方法は、「__変数の管理__ が __大変__ になる」、「テストを行う際に、__一部__ のテストケースを使うことを __忘れてしまう__ 可能性が生じる」などの __欠点__ があるため、あまりお勧めしません。

また、一度 __テストを行って__、`judge` メソッドが __期待される処理を行う__ ことが __確認__ されたテストケースは、__二度と利用する必要__ がないので、__複数の変数__ でテストケースを __管理しても良い__ のではないかと __思う人がいるかもしれません__ が、それは __大きな間違__ いです。

これまでの記事で何度も行ってきたように、一度 __定義した関数__ を、__後から修正__ することは良くある事です。__関数を修正__ した場合は、当然ですが、修正した関数が __正しく動作__ するかを __テストする必要__ があります。全てのテストケースを __まとめて記述__ し、`testcases` のような __変数に代入__ ておくことで、テストの __やり直し__ を `test_judge(testcases)` を __呼び出すだけ__ で __簡単に実行__ できます。

一方、__全て__ のテストケースを __まとめて記述__ するという方法では、__`test_judge`__ で __テストを行うたび__ に、__すべて__ のテストケースに対応する __ゲーム盤が表示__ されてしまうという __問題__ があります。ただし、この問題は、`test_judge` を __改良__ することで __簡単に解決__ することができます。

## `test_judge` の改良 その 1（ゲーム盤の表示の有無の選択）

具体的には、`test_judge` に、__ゲーム盤__ を __表示するどうか__ を表す __仮引数を追加__ するという方法です。この方法では、__仮引数の値__ が __`True`__ の場合 __のみ__ 、`test_judge` で __ゲーム盤を表示__ します。

このような目的の __仮引数の名前__ として、__ゲーム盤の表示__ が、正しいマスにマークが配置されていないという __バグを発見__ するという、__デバッグ__ のための __表示__ であることから __`debug`__ という名前が __良く使われる__[^1]ので、本記事でもこの名前にします。また、`test_judge` で __ゲーム盤を表示__ する __必要がある__ のは、__テストケース__ を __新しく記述__ した __場合だけ__ なので、この仮引数を __[デフォルト引数](https://qiita.com/ysgeso/items/c5dbe62b329f7936181a#%E3%83%87%E3%83%95%E3%82%A9%E3%83%AB%E3%83%88%E5%BC%95%E6%95%B0%E3%81%A8%E3%83%87%E3%83%95%E3%82%A9%E3%83%AB%E3%83%88%E5%BC%95%E6%95%B0%E5%80%A4)__ とし、__デフォルト値__ を __`False`__ とします。

下記は、`test_judge` を修正したプログラムです。__修正箇所__ は、__仮引数 `debug` の追加__ と、10
行目に __if 文__ を記述し、__`debug`__  が __`True` の場合__ のみ __ゲーム盤を表示__ するようにした点です。

```python
 1  from test import excel_to_xy
 2
 3  def test_judge(testcases, debug=False):
 4     for winner, testdata_list in testcases.items():
 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                  print("ok")
15              else:
16                  print("test_judge error!")
17                  print("mb.judge():", mb.judge())
18                  print("winner:    ", winner)
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
from test import excel_to_xy

def test_judge(testcases, debug=False):
    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)
            if debug:
                print(mb)

            if mb.judge() == winner:
                print("ok")
            else:
                print("test_judge error!")
                print("mb.judge():", mb.judge())
                print("winner:    ", winner)
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
from test import excel_to_xy

-def test_judge(testcases):
+def test_judge(testcases, debug=False):
    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 debug:
+               print(mb)

            if mb.judge() == winner:
                print("ok")
            else:
                print("test_judge error!")
                print("mb.judge():", mb.judge())
                print("winner:    ", winner)
```
</details>

下記のプログラムのように、__2 つ目__ の __実引数__ に __`True` を記述__ して `test_judge` を呼び出すことで、__ゲーム盤を表示__ したテストが行われます。実行結果は先ほどと同じなので省略します。

```python
test_judge(testcases, True)
```

__2 つ目の実引数__ が何を意味するかが __分かりにくい__ と思った方は、下記のように、__[キーワード引数](https://qiita.com/ysgeso/items/c5dbe62b329f7936181a#%E4%BD%8D%E7%BD%AE%E5%BC%95%E6%95%B0%E3%81%A8%E3%82%AD%E3%83%BC%E3%83%AF%E3%83%BC%E3%83%89%E5%BC%95%E6%95%B0)__ を使って、記述すると良いでしょう。本記事でもわかりやすさを重視してこのように記述します。 

```python
test_judge(testcases, debug=True)
```

`debug` は __デフォルト値__ に __`False` が設定__ された __デフォルト引数__ なので、下記のように `debug` に対応する __実引数を省略__ することで、__ゲーム盤を表示せず__ に __テストを行う__ ことができます。

```python
test_judge(testcases)
```

実行結果
```
ok
ok
ok
ok
ok
ok
ok
ok
ok
ok
ok
ok
ok
ok
ok
ok
ok
```

## `test_judge` の改良 その 2（期待された処理が行われた場合の表示）

先程の実行結果では、`judge` メソッドが __期待された値を返した__ ことを表す __`"ok"`__ が __大量に表示__ されましたが、テストで __知りたいこと__ は、[以前の記事](https://qiita.com/ysgeso/items/728cee8982fe0ed9da38#%E3%83%86%E3%82%B9%E3%83%88%E3%81%AE%E6%89%8B%E9%A0%86) で説明したように、__期待された処理__ が __行われなかった__ テストケースが __あるかどうかを調べること__ なので、期待された処理が行われたことを表す `"ok"` を表示しても __あまり意味はありません__。また、テストでは、一般的に __ほとんど__ のテストケースに対して __期待された処理が行われる__ ので、そのことを __画面に表示__ すると、上記のように画面が __メッセージで埋め尽くされる__ ことになり、__肝心__ の __エラーメッセージ__ が __埋もれて__ わからなくなってしまう __可能性が高く__ なります。

そこで、下記のプログラムのように、`test_judge` で __期待された処理__ が __行われた場合__ は、__メッセージ__ を __表示しない__ ように __修正__ することにします。具体的には、12 行目の `print("ok")` を __ブロックの中__ で __何の処理も行わない__ ことを表す __`pass` に修正__ します。

```python
 1  def test_judge(testcases, debug=False):
 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             if debug:
 9                 print(mb)
10
11              if mb.judge() == winner:
12                  pass
13              else:
14                  print("test_judge error!")
15                  print("mb.judge():", mb.judge())
16                  print("winner:    ", winner)
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
def test_judge(testcases, debug=False):
    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)
            if debug:
                print(mb)

            if mb.judge() == winner:
                pass
            else:
                print("test_judge error!")
                print("mb.judge():", mb.judge())
                print("winner:    ", winner)
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
def test_judge(testcases, debug=False):
    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)
            if debug:
                print(mb)

            if mb.judge() == winner:
-               print("ok")
+               pass
            else:
                print("test_judge error!")
                print("mb.judge():", mb.judge())
                print("winner:    ", winner)
```
</details>

修正後に、`test_judge` を実行すると、下記のように、何も表示されなくなります。

```python
test_judge(testcases)
```
実行結果
```
```

:::note
ブロックの中で何の処理も行わない場合は、__必ず `pass` を記述__ する __必要__ があり、`pass` を __省略__ すると、下記のプログラムのように __エラーが発生__ します。

```python
if True: # この if 文のブロックが記述されていない
a = 1
```
実行結果
```
  Cell In[13], line 2
    a = 1
    ^
IndentationError: expected an indented block after 'if' statement on line 1
```

上記のエラーメッセージは、以下のような意味を持ちます。

* __IndentationError__
インデント（indent）に関するエラー
* __expected an indented block__
インデントされた（indented）ブロック（block）が期待されている（expected）

pass がわかりにくいと思った人は、下記のように、`judge` メソッドの返り値と、期待される返り値が __等しくない__ 場合にエラーメッセージを表示するようにすると良いでしょう。

```python
            if mb.judge() != winner:
                print("test_judge error!")
                print("mb.judge():", mb.judge())
                print("winner:    ", winner)
```

<details><summary><strong>修正箇所</strong></summary>

```diff_python
-           if mb.judge() == winner:
-               pass
-           else:
+           if mb.judge() != winner:
                print("test_judge error!")
                print("mb.judge():", mb.judge())
                print("winner:    ", winner)
```
</details>
:::

## `test_judge` の改良 その 3（途中経過の表示）

先程の改良によって、すべてのテストケースで `judge` メソッドが期待された値を返す場合は、何も __表示が行われなく__ なりますが、何かの処理を行った結果、__何も表示されない__ と、うまくいったかどうかがわからないので、__不安になる__ 人が多いのではないかと思います。

そのような場合は、__途中経過__ を表す __メッセージを表示__ するのが一般的です。

どのようなメッセージが __わかりやすい__ かについては、__人によって違う__ ので唯一の正解はありません。本記事では、下記のようなメッセージを表示することにしますが、もっと良いメッセージを思いついた人は、自分なりに自由にアレンジして下さい。

* __テストを開始__ したことを表示する（2 行目）
* __期待される返り値__ が __変わった時__ に、__その値__ を表示する（4 行目）
* `judge` メソッドの返り値が __期待される返り値の場合__ に、そのことを __短く表示__ する（14 行目）
* __テストを終了__ したことを表示する（20 行目）

下記は、上記のメッセージを表示するように修正したプログラムです。上記の（）の中は、それぞれのメッセージを表示するプログラムを記述した行数を表します。

```python
 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                  print("o", end="")
15              else:
16                  print("test_judge error!")
17                  print("mb.judge():", mb.judge())
18                  print("winner:    ", winner)
19          print()
20      print("Finished")
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
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="")
            else:
                print("test_judge error!")
                print("mb.judge():", mb.judge())
                print("winner:    ", winner)
        print()
    print("Finished")
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
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:
-               pass
+               print("o", end="")
            else:
                print("test_judge error!")
                print("mb.judge():", mb.judge())
                print("winner:    ", winner)
+       print()
+   print("Finished")
```
</details>

__テストケースの数__ があまり __多くない__ 場合は、上記の 14 行目のように、`judge` メソッドの返り値が期待される返り値であった場合に、__`"o"`__ のような __短いメッセージを表示__ することで、テストが行われていることを __実感できるようにする__ ことができます。ただし、__テストケースの数__ が、例えば 10000 のように __非常に多い__ 場合は、このようなメッセージは __表示しないほうが良い__ でしょう。なお、`print` で `"o"` を __改行せずに表示__ するために、[`end=""` を実引数に記述](https://qiita.com/ysgeso/items/c5dbe62b329f7936181a#print-%E3%81%AE%E4%BB%AE%E5%BC%95%E6%95%B0-end-%E3%81%AE%E6%84%8F%E5%91%B3)しています。

19 行目の `print()` は、3 行目の for 文の __次の繰り返し__ を __行う前__ に、__改行を行う__ ために記述しています。これを __記述しない__ と、14 行目で表示した __`"o"` の直後__ に 4 行目の __`print` のメッセージが表示__ されてしまいます。具体例はこの後のノートを参照して下さい。

修正後に、`test_judge` を実行すると、下記のようなメッセージが表示されるようになります。`test winner =` の次の行の __`"o"` の数__ は、__期待される返り値__ に __対応するテストケースの数__ です。

```python
test_judge(testcases)
```
実行結果
```
Start
test winner = playing
o
test winner = o
oooooooo
test winner = x
oooooooo
Finished
```

:::note
19 行目の `print()` を記述しないと、実行結果は以下のように、__`test winner` の前__ などに __`"o"` が表示__ されるようになります。興味がある方は、実際に 19 行目を削除して `test_judge` を実行してみて下さい。

```
Start
test winner = playing
otest winner = o
ooooooootest winner = x
ooooooooFinished
```
:::

## `test_judge` の改良 その 3 の問題点

先程実行した、`test_judge(testcases)` では、テストケースの中に、下記の __if 文__ の __条件式__ が __`False` になる__ ようなものが __存在しない__ ため、__`else` のブロック__ が __一度も実行されていません__ でした。そのため、__改良後__ の `test_judge` で、下記の if 文の条件式が __`False` になった場合__ にどのような __メッセージが表示__ されるかを __確認__ しておいたほうが良いでしょう。

これは、__すべての文__ を __1 度は実行する__ という、__[命令網羅(C0)](https://qiita.com/ysgeso/items/728cee8982fe0ed9da38#%E5%91%BD%E4%BB%A4%E7%B6%B2%E7%BE%85statement-coverage)__ のテストを行うということです。

```python
            if mb.judge() == winner:
                print("o", end="")
            else:
                print("test_judge error!")
                print("mb.judge():", mb.judge())
                print("winner:    ", winner)
```

この if 文の条件式が `False` になるのは、`judge` メソッドが __期待される返り値__ を __返さない__ 場合です。__確認の方法__ として、__`judge` メソッド__ を __バグのある状態に戻す__ という方法が考えられますが、それよりも、__テストケースのデータ__ のほうを __間違ったデータに修正__ したほうが __簡単__ です。

そこで、テストケースを下記のように、__わざと間違ったデータ__ に __修正__ します。具体的には、下記のプログラムの、コメントが書かれている行のテストデータの最後に記述されていた __`"B3"` を削除__ することで、__〇 が勝利していない__ テストデータに修正しています。なお、このデータは正しいデータが代入された `testcases` と __区別ができる__ ように、__`testcases_error`__ という変数に代入します。

```python
testcases_error = {
    # ゲーム盤に一つもマークが配置されていない場合のテストケース
    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",
    ],
    # × の勝利のテストケース
    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",
    ],
}
```

修正後に、`test_judge` を実行すると、下記のようなメッセージが表示されます。このエラーメッセージが __わかりにくい__ 主な __理由__ が 2 つあるので、その理由について少し考えてみて下さい。

```python
test_judge(testcases_error)
```
実行結果
```
Start
test winner = playing
o
test winner = o
ooootest_judge error!
mb.judge(): playing
winner:     o
ooo
test winner = x
oooooooo
Finished
```

メッセージがわかりにくい __主な理由__ は以下の 2 点です。

* `ooootest_judge error!` の行で、`oooo` と `test_judge error!` の間で __改行されていない__
* エラーメッセージの前に、__ゲーム盤__ が __表示されていない__ ので、__どのような状況__ でエラーが発生したかが __わからない__


一つ目は、`test_judge error!` の __直前で改行__ を表示することで解決できます（16 行目）。

二つ目は、`test_judge error!` の __後で__ 、__ゲーム盤を表示__ することで解決できます（18 行目）。

下記は、そのように修正したプログラムです。上記の（）の中が修正箇所を表します。

```python
 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                  print("o", end="")
15              else:
16                  print()
17                  print("test_judge error!")
18                  print(mb)
19                  print("mb.judge():", mb.judge())
20                  print("winner:    ", winner)
21          print()
22      print("Finished")
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
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="")
            else:
                print()
                print("test_judge error!")
                print(mb)
                print("mb.judge():", mb.judge())
                print("winner:    ", winner)
        print()
    print("Finished")
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
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="")
            else:
+               print()
                print("test_judge error!")
+               print(mb)
                print("mb.judge():", mb.judge())
                print("winner:    ", winner)
        print()
    print("Finished")
```
</details>

修正後に、`test_judge` を実行すると、下記のようなメッセージが表示されます。

```python
test_judge(testcases_error)
```
実行結果
```
Start
test winner = playing
o
test winner = o
oooo
test_judge error!
Turn o
xo.
xo.
...

mb.judge(): playing
winner:     o
ooo
test winner = x
oooooooo
Finished
```

修正後のメッセージは、__修正前より__ は __わかりやすく__ なっていますが、__エラーメッセージ__ と __そうでないメッセージ__ の __区別がつきづらい__ 点がまだ __わかりづらい__ のではないかと思います。

このような場合は、__エラーメッセージ__ の __前後__ で __改行__ したり、下記のプログラムの 17、22 行目のように、エラーメッセージの __境目__ が __明確になるような表示__ を行うという工夫が考えられます。

```python
 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                  print("o", end="")
15              else:
16                  print()
17                  print("====================")
18                  print("test_judge error!")
19                  print(mb)
20                  print("mb.judge():", mb.judge())
21                  print("winner:    ", winner)
22                  print("====================")
23          print()
24      print("Finished")
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
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="")
            else:
                print()
                print("====================")
                print("test_judge error!")
                print(mb)
                print("mb.judge():", mb.judge())
                print("winner:    ", winner)
                print("====================")
        print()
    print("Finished")
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
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="")
            else:
                print()
+               print("====================")
                print("test_judge error!")
                print(mb)
                print("mb.judge():", mb.judge())
                print("winner:    ", winner)
+               print("====================")
        print()
    print("Finished")
```
</details>

修正後に、`test_judge` を実行すると、下記のようなメッセージが表示されます。

```python
test_judge(testcases_error)
```
実行結果
```
Start
test winner = playing
o
test winner = o
oooo
====================
test_judge error!
Turn o
xo.
xo.
...

mb.judge(): playing
winner:     o
====================
ooo
test winner = x
oooooooo
Finished
```

ずいぶんとわかりやすくなったと思いますので、本記事ではこれを採用することにします。

上記で満足できない方は、自分がわかりやすいと思えるようになるまで、自由に修正して下さい。例えば、期待される返り値が変わった時に表示する、`test winner = o` の前で改行するなどの工夫が考えられるでしょう。

:::note
上記の 16 行目のプログラムは改行を表示するプログラムですが、改行を表す __`\n`__ という __[エスケープシーケンス](https://qiita.com/ysgeso/items/ed1eb1cacf9a3e7dfa29#__str__-%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89)__ を使うことで、下記の __2 行をまとめる__ ことが出来ます。
```python
16                  print()
17                  print("====================")
```

具体的には下記のように記述します。

```python
print("\n====================")
```
<details><summary><strong>修正箇所</strong></summary>

```diff_python
-print()
-print("====================")
+print("\n====================")
```
</details>

:::

# 今回の記事のまとめ

今回の記事では、__× の勝利__ を判定する if 文に対する MC/DC のテストを行い、__`test_judge` のバグ__ の一つを __発見__ し、__修正__ しました。また、__`test_judge` を改良__ しました。

大変申し訳ありませんが、`test_judge` の改良が思いのほか長くなったので今回の記事はここまでにしたいと思います。次回で、テストに関する記事を終了したいと思います。


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

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

https://github.com/ysgeso/marubatsu/blob/master/032/marubatsu.ipynb

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

https://github.com/ysgeso/marubatsu/blob/master/032/marubatsu_new.py

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

https://github.com/ysgeso/marubatsu/blob/master/032/test_new.py

# 次回の記事

https://qiita.com/ysgeso/items/8468e6bc59b7595c21f0

[^1]:他にも、詳細という意味を表す `verbose` という名前が使われるようです