テスト・ケース制作ツールの制作記(愛憎のAllpairs.py)


テスト設計手法の基本はコンディションの組み合わせによります。

テスト設計手法を勉強したことある方はどのくらい共感できると思います。境界値分析、同値分割はそうじゃないけど決定テーブル、状態遷移はもちろん、コード・ベース設計手法もTRUEとFALSEの組み合わせです。

人気がある言語のJavaとかPythonでそんな組み合わせライブラリがあるはずです。僕はPython馴染みなのでPyPIで検索してみました。やっぱりありました[^1]。

![](https://t1.daumcdn.net/thumb/R1280x0/?fname=http://t1.daumcdn.net/brunch/service/user/J8k/image/_dspvrwOQ-ttPIzoqdo74_-iuLc.PNG)

ソースコードで条件(if, whileなど)を読み込んで組み合わせをつくりました。例えば

```py
if (A>0 || B==0){ //decision #1
    statement1;
}

if (C>0 && D==0){ //decision #2
    statement2;
}
```

でCondition Coverageを満足させる統合ケースを作るためにはディシジョン＃1のTrue/Falseケース（各4個）とディシジョン＃2のケース（4個）の組み合わせです。ディシジョン間の組み合わせはプールではなくて各ケースが一回だけ現れると良いので最後にケースの数は4個になります。

* ディシジョン#1の組み合わせ数

| A>0  | B==0 | ディシジョン#1 |
| :--: | :--: | :------------: |
|  T   |  T   |       T        |
|  T   |  F   |       T        |
|  F   |  T   |       T        |
|  F   |  F   |       F        |

* ディシジョン#2の組み合わせ数

| C>0  | D==0 | ディシジョン#2 |
| :--: | :--: | :------------: |
|  T   |  T   |       T        |
|  T   |  F   |       F        |
|  F   |  T   |       F        |
|  F   |  F   |       F        |

* ディシジョン#1×ディシジョン#2の統合ケース

|  No  | A>0  | B==0 | C>0  | D==0 |
| :--: | :--: | :--: | :--: | :--: |
|  1   |  T   |  T   |  T   |  T   |
|  2   |  T   |  F   |  T   |  F   |
|  3   |  F   |  T   |  F   |  T   |
|  4   |  F   |  F   |  F   |  F   |

今度のケースは各ディシジョンの関係が独立的なのでお互いに営業をあたえられなかったんです。でもNested構文では話が変わります。

```
if (A>0 || B==0){ //ディシジョン #1
    statement1;
    
    if (C>0 && D==0){ //ディシジョン #2
        statement2;
    }
}
```

 　

ディシジョン＃2は、ディシジョン＃1がTrueのときだけTrueまたはFalseが判断されることができます。＃1がFalseである場合には、#2はスキップされるので判断ができません。前ではちょうど2つの条件の結果の組み合わせを作ったら良かったですが、今回は組み合わせに制約をかけなければならある。

結果は以下のようになるだろう。

|  No  | A>0  | B==0 | ディシジョン#1 | C>0  | D==0 | ディシジョン#2 |
| :--: | :--: | :--: | :------------: | :--: | :--: | :------------: |
|  1   |  T   |  T   |       T        |  T   |  T   |       T        |
|  2   |  T   |  F   |       T        |  T   |  F   |       F        |
|  3   |  F   |  T   |       T        |  F   |  T   |       F        |
|  4   |  F   |  F   |       F        | Skip | Skip |       -        |
|  5   |  T   |  T   |       T        |  F   |  F   |       F        |

この他にも、コードの条件節間での制約をかけるべきところがいくつかではあります[^2]。そこまでここに記述しません。



allpairs.pyライブラリでは、（当然ですが）組み合わせに制約をかける機能（フィルタリング）があります。サンプルコードは、以下の通りである。

```py
from allpairspy import AllPairs

def is_valid_combination(row):
    """
    This is a filtering function. Filtering functions should return True
    if combination is valid and False otherwise.

    Test row that is passed here can be incomplete.
    To prevent search for unnecessary items filtering function
    is executed with found subset of data to validate it.
    """

    n = len(row)

    if n > 1:
        # Brand Y does not support Windows 98
        if "98" == row[1] and "Brand Y" == row[0]:
            return False

        # Brand X does not work with XP
        if "XP" == row[1] and "Brand X" == row[0]:
            return False

    if n > 4:
        # Contractors are billed in 30 min increments
        if "Contr." == row[3] and row[4] < 30:
            return False

    return True

parameters = [
    ["Brand X", "Brand Y"],
    ["98", "NT", "2000", "XP"],
    ["Internal", "Modem"],
    ["Salaried", "Hourly", "Part-Time", "Contr."],
    [6, 10, 15, 30, 60]
]

print("PAIRWISE:")
for i, pairs in enumerate(AllPairs(parameters, filter_func=is_valid_combination)):
    print("{:2d}: {}".format(i, pairs))
```



ところが、ここにロジック上のエラーが少しありました。ここで詳細に言及しないですが、フィルタリングする条件が複数があるとき、その順序を変更すると、ペアリングの結果が変わったり欠落している場合が発生しました[^3]。ここで悩みは

* ロジックを直接に修正するか。
* ロジックは修正しなくてフィルタリングの順番を自動的に調整させて結果をもらうか。

私はロジックを変更する自身がまったくありませんでしたので[^4]第二の道を選択しましたが、それも簡単ではないでした。自動調整も考えなければならない場合の数が多いし、なんと6ヶ月近くの時間がかかりました。そうしたらallpairs.pyに愛憎ができちゃいました。



そこで誕生したのが今はgitHubから全部削除しましたが、TCaseGeneratorというアプリでした。当時はそれなりMC/ DCをフェイクで実装しました。ウィンドウズとMacOSの用のアプリをPyInstallerで作って、当時は私の顧客にダウンさせて頂きました。無数のエラーを抱えているアプリを...



[^1]: 開発を開始する時には、Allpairs2.0.1だけでした。今新たにForkされたallpairspy2.4.3が出て、これを使用すればいいです。
[^2]: 「else if」,「nested else if」,「コンディション変数の再使用」などです。どちでも難しいでした。
[^3]: 当時にはallpairs.pyを使用しました。新たにForkされたallpairspy.pyではまだテストしませんが、問題部分のコードがそのまま見たいなので問題がまだ存在する可能性が高いです。
[^4]: 原因は分かりましたが、Side Effectがとっても怖いでした。