開発設計者やプログラマにも知っておいて欲しいこと
テスト設計技術者に任せっ放しでは絶対に上手くいかないので、開発設計者やプログラマにも知っておいて欲しいことを出来るだけ簡潔に以下に説明します。
原因結果グラフ法がテストを減らすときの「考え方」と、MCC(multiple condition coverage)に対してMC/DC(Modified Condition/Decision Coverage)がテストを減らすときの「考え方」は同じです。
なので、某書籍の原因結果グラフを解説した章の中で「この方法で生成したテスト・ケースはMCDCカバレッジが 100%になる」と記載されていたりするのですが、それは少し不親切で、そんなに単純な話でもありません。
テスト設計においてデシジョンテーブルを圧縮するときには、それが実装側の条件の処理順と整合するように しなければならなかったことを思い起こしてみてください。
原因結果グラフ法の場合も同様です。ですから、「実装側と整合するようにグラフを作成すれば、生成されたテスト・ケースでMC/DCカバレッジは100%になる。」ぐらいが適切な表現になるのではないかと思います。
デシジョンテーブルの圧縮と同様、テスト設計側と開発設計・実装側との協力が欠かせないので、そこは理解しておいて頂きたいところです。
また、これを面倒なこととは考えないで欲しいのです。この整合の確認やレビューの過程こそが、テストよりも品質向上に貢献することが多いです。当にミスが起こり易いポイントを、実装側と検証側で立場の違う者の間で確認し合うことになるので、バグの作り込み防止に間違いなく有効となります。
グラフと実装コードを整合させるとはどういうことか
さて、「実装側と整合するようにグラフを作成すれば」とは、どういうことであるのかを次の事例で考えてみることにしましょう。
if (16 <= age) or (12 <= age and parentalConsent):
print("ワクチン接種できます")
else:
print("ワクチン接種できません)
他の言語でも良いわけですが、説明のために何か選ぶ必要があるので、Pythonを選びました。ageには対象者の年齢が、parentalConsentには親の同意があるかどうかがブール値で入っているものとします。
この実装を知らずに原因結果グラフを作るとどうなるでしょうか?
仕様として「来場者の年齢が16歳以上であればワクチン接種できる。また、12歳以上で保護者の同意があればワクチン接種できる。」ぐらいの情報があるものとして、グラフを描いてみましょう。
まず、次のように命題表現に直します。
- P1
- 来場者の年齢は16歳以上である
- P2
- 来場者の年齢は12歳以上である
- P3
- 来場者の保護者の同意がある
この命題を使って作成したグラフは、以下の通りになります。(CEGToolを使用して作成しました。)
できるだけ簡潔にしたかったので、ワクチン接種できる方だけを、そしてノード名は論理式「P1∨(P2∧P3)」のまま表現してあります。
もちろん、命題表現への直し方も、グラフの形も、違うものを考える人はいると思いますが、「ここに示したようなものを作成することは、ありそうなことだ」と思って頂ければ、以降の説明を分かっていただけると思います。
ツールが生成したデシジョンテーブルには#1~#3の3つのテストケースが示されています。これでMC/DCカバレッジ100%が達成できるでしょうか?
実はこれ、達成できない事例として示しています。なぜなら、実装側と整合するようにグラフが描かれていないからです。
このグラフは割と素直な表現ではないかと思うのですが、注意深く見ると、論理積P2∧P3の結果を参照し、それとP1の論理和をとって結果を導いています。
つまり、グラフと整合する実装は、最初に示したものとは異なり、以下のようなコードになります。
if (12 <= age and parentalConsent) or (16 <= age):
print("ワクチン接種できます")
else:
print("ワクチン接種できません)
下のフロー図で見て頂いた方が分かり易いかと思います。
ここで、省略も圧縮もしていないデシジョンテーブルを以下に示します。上記フローの通ったパスの矢印に色を付けてありますが、デシジョンテーブルの対応するマスに同じ色を付けてあります。また、CEGToolが生成したテストケースの番号を最上行に記載しています。
これを見れば、#1~#3の3つのテストケースで、全ての色の矢印を通り、MC/DCカバレッジ100%が確かに達成されていることがわかります。
これで問題は解決しました。最初の「実装コード例1」をやめて、グラフに整合するように後に示した方の「実装コード例2」に変えればよいわけです。
ここまでの説明とは逆に、グラフを実装に合わせることにすると
しかし、これでは納得がいかない方もいると思います。
「16歳以上の人の方が多いはずだから、最初の実装の方が効率が良いのではないの?」と思われた方もいるでしょう。(まあ、この事例でそんなに拘りはないかもしれませんが、実務では拘るべきケースも出てくるはずです。)
では、今度は反対に最初の「実装コード例1」にグラフの方を整合させることを考えてみましょう。つまり、フロー図としては次のようにしたいとします。
P1の結果を見てから論理積をとるようにすれば良いわけです。つまり、グラフはP1の結果を参照するように以下のように修正すれば良いです。
これについても、省略も圧縮もしていないデシジョンテーブルを以下に示します。同じように上記フローの通ったパスの矢印に色を付けてあり、デシジョンテーブルの対応するマスに同じ色を付けてあります。また、CEGToolが生成したテストケースの番号を最上行に記載しています。
今度はCEGTestが生成したテストケースは4つになりますが、これでMC/DCカバレッジ100%が達成できていることが確認できました。
冗長な表現はよく考えて方針を決めてからにした方が良い
ところで、先のプログラム表現をグラフ表現に過度に合わせようとすると別の問題が発生します。
先の「P1∨(¬P1∧(P2∧P3))」の¬P1の部分を、改めて実装に含めようとすると、(もう整合済みのはずの上に記述を重ねることになるので)冗長な表現となります。
これは株式会社デンソーの榎本秀美氏によりソフトウェア品質シンポジウム2015で『記号実行を用いたテストデータ自動生成の試行評価』の中で報告されている図5の冗長なロジック例に該当する結果となります。
このように、可読性向上に繋がることもある一方で、冗長な表現は、MC/DCカバレッジの邪魔にもなってしまいます。
論理的整合、処理(振る舞い)的整合、表現的整合は、ここまで示してきた通り、少しずつ違います。このことをよく理解したうえで、また、コーディング規約などとも合わせて、整合の方針を決めておくのが良いでしょう。
分かり易さのための冗長な表現はコメントアウトとする方針も考えられます。
同じことですが、示してきた事例で1行目を次のように記述することも考えられます。
if (16 <= age) or (12 <= age and age < 16 and parentalConsent):
さらに言えば、次のように記述することもできます。
if (12 <= age and age < 16 and parentalConsent) or (16 <= age) :
MC/DCカバレッジのために、この変更がどう影響するか、考えてみるとより一層理解が深まると思います。
おまけ:論理式とプログラムの条件式との違い
論理式としては交換律(commutative law)が成り立つので、P1∨(P2∧P3) と (P2∧P3)∨P1 は、同じになるはずですが、プログラミング言語の処理系ではそうなるとは限りません。必要ない計算は実行されないことがあります。
これは初心者を除けばプログラマは皆さん知っていることで、ただ知っているどころかそのことを利用するようなコードを書いているのが普通でしょう。
しかし一方、コンサルや教育で行った先々で今回のような説明をすると、括弧()で括ればそこが先に計算されると勘違いをしている人にしばしば出会うことがあります。
四則演算で乗除の演算子は加減の演算子より優先順位が高いことや、括弧()で優先順位を明示できること、さらには()の中から先に計算すると良い等と教わった知識が邪魔をするのではないかと思います。(ついでに述べておくと、プログラミング言語ではandがorより優先順位が高い演算子に位置づけられていることが多いですが、論理式では特には決まっていません。)
括弧()は結合の優先度(強さ)を表すものであり、時間的に先に評価(計算)することを要求するものではないことにも注意が必要です。
Python3の場合は、記述の左側から先に処理していることを、以下のように入力を与えて結果を見ることで確認できます。
$ python
Python 3.8.5 (default, Sep 4 2020, 07:30:14)
[GCC 7.3.0] :: Anaconda, Inc. on linux
>>> age=16
>>> (16 <= age) or (12 <= age and parentalConsent)
True
>>> (12 <= age and parentalConsent) or (16 <= age)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'parentalConsent' is not defined
>>> age=10
>>> (16 <= age) or (12 <= age and parentalConsent)
False
>>> (12 <= age and parentalConsent)or(16 <= age)
False
>>> age=12
>>> (16 <= age) or (12 <= age and parentalConsent)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'parentalConsent' is not defined
>>> (12 <= age and parentalConsent)or(16 <= age)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'parentalConsent' is not defined
>>>
これはつまり、わざとparentalConsentを未定義状態にしてあります。parentalConsentを参照する前に結果が確定する計算が済んでしまえばエラーは起きないわけなので、どこが先に計算されたかわかります。