カバレッジとアサーション
実行範囲の指標と不具合の検出を区別し、カバレッジが100%でも期待値の検査が必要な理由を説明します。
説明
カバレッジは、実行したコードの範囲を測る指標です。実行した結果を正しく検査したかまでは表しません。すべての行を通った場合でも、条件の片側しか試していなかったり、誤った金額を見逃したりすることがあります。
会員は10%引き、非会員は元の金額という料金関数を使います。Coverage.py 7.17.2で行と分岐を測り、割引率を誤って20%へ変えた実装に対して、型だけを確認する試験と金額まで確認する試験を比較します。
実行範囲の指標と不具合の検出を区別し、カバレッジが100%でも期待値の検査が必要な理由を説明します。
1料金関数と型だけの試験
関数は会員の場合に金額の90%を整数へ切り捨てます。one.pyは会員だけを呼び、戻り値がintかを調べます。
cat pricing.py条件分岐のある関数を読む
cat one.py試験が確認する内容を読む計算結果が90円かどうかは確認していません。また、非会員の分岐を通る入力もありません。この二点を測定と変異で調べます。
2行の実行範囲は100%になる
対象をpricingに限定して計測し、レポートを表示します。
python3 -m coverage run --source=pricingpricingの実行を計測する one.py
python3 -m coverage report -m未実行箇所を含む集計を表示する実行対象の文は5、未実行は0で100%です。会員の入力一つで全行を通っていますが、ifが偽になる経路はまだ通っていません。
3分岐を含めて測る
--branchを付けて同じ入力を実行します。前の計測結果は新しい実行で置き換わります。
python3 -m coverage run --branch分岐先への遷移も計測する --source=pricing one.py
python3 -m coverage report -m文と分岐を合わせた集計を読む分岐先は2通りで、一方が未実行です。3->5はifを偽として次のreturnへ進む経路を表します。Coverの86%は文5件と分岐2件を合わせた6/7であり、分岐だけの割合は1/2です。
4両側を通して100%にする
both.pyでは非会員も呼びますが、どちらも戻り値の型だけを確認しています。
cat both.py二つの入力で何を確認するか読む
python3 -m coverage run --branch両分岐の実行を計測する --source=pricing both.py
python3 -m coverage report -m文も分岐もすべて通り、100%になります。ここで分かったのは実行範囲です。90円と100円を返したかは別に確認する必要があります。
5割引率を誤らせても試験が通る
10%引きを20%引きへ変えた試料を反映します。型だけの試験を再実行します。
cp pricing-mutant.py pricing.py割引率を変更した実装へ替える
python3 -m coverage run --branch同じ試験を再実行する --source=pricing both.py
python3 -m coverage report -m計算規則を誤らせても試験は成功し、100%のままです。80円もintなので型の検査に引っ掛かりません。コードへ小さな誤りを入れて試験の検出力を調べる方法を変異テストと呼びます。
6実際の金額を取り出す
会員と非会員の計算結果を直接表示します。
python3 -c "from pricing import price; print(price(100,True),price(100,False))二つの実際の金額を表示する"会員80円、非会員100円です。要求した会員90円と一致していないことが、具体的な値から分かります。
7金額を確認する試験なら検出できる
unittestで会員90円、非会員100円、101円の会員価格90円を確認します。
python3 run_tests.py test_pricing.Values具体的な金額と比較する会員と丸めのケースがFAILになります。型だけでなく期待する値を検査したことで、同じ100%の実行範囲でも誤った計算を見つけられました。
8正しい実装で再確認する
元の10%引きへ戻し、金額を確認する試験を分岐計測付きで実行します。
cp pricing-original.py pricing.py元の割引規則へ戻す
python3 -m coverage run --branch --source=pricing run_tests.py test_pricing.Values値の検査を実行しながら計測する
python3 -m coverage report -m3件が成功し、分岐を含むカバレッジも100%です。ただし試した金額は一部です。高いカバレッジと期待値の検査を組み合わせ、仕様上の境界や異常条件も別に検討します。
まとめ
行カバレッジ100%でも、条件の偽側を通っていない場合があります。
行と分岐が100%でも、型しか検査しない試験は誤った金額を見逃します。
変異を検出できるかと、期待値が要求を表しているかを確認し、実行範囲だけで品質を判断しません。
操作履歴と解説を振り返れます。
シェル / Python 3
cat pricing.py
cat one.pydef price(amount,member):
result=amount
if member:
result=amount*90//100
return result
from pricing import price
assert isinstance(price(100,True),int)python3 -m coverage run --source=pricing one.py
python3 -m coverage report -mName Stmts Miss Cover Missing ------------------------------------------ pricing.py 5 0 100% ------------------------------------------ TOTAL 5 0 100%
python3 -m coverage run --branch --source=pricing one.py
python3 -m coverage report -mName Stmts Miss Branch BrPart Cover Missing -------------------------------------------------------- pricing.py 5 0 2 1 86% 3->5 -------------------------------------------------------- TOTAL 5 0 2 1 86%
cat both.py
python3 -m coverage run --branch --source=pricing both.py
python3 -m coverage report -mfrom pricing import price assert isinstance(price(100,True),int) assert isinstance(price(100,False),int) Name Stmts Miss Branch BrPart Cover Missing -------------------------------------------------------- pricing.py 5 0 2 0 100% -------------------------------------------------------- TOTAL 5 0 2 0 100%
cp pricing-mutant.py pricing.py
python3 -m coverage run --branch --source=pricing both.py
python3 -m coverage report -mName Stmts Miss Branch BrPart Cover Missing -------------------------------------------------------- pricing.py 5 0 2 0 100% -------------------------------------------------------- TOTAL 5 0 2 0 100%
python3 -c "from pricing import price; print(price(100,True),price(100,False))"80 100
python3 run_tests.py test_pricing.ValuesFAIL test_pricing.Values.test_member: 80 != 90 PASS test_pricing.Values.test_nonmember FAIL test_pricing.Values.test_rounding: 80 != 90 tests=3 failures=2 errors=0
cp pricing-original.py pricing.py
python3 -m coverage run --branch --source=pricing run_tests.py test_pricing.Values
python3 -m coverage report -mPASS test_pricing.Values.test_member PASS test_pricing.Values.test_nonmember PASS test_pricing.Values.test_rounding tests=3 failures=0 errors=0 Name Stmts Miss Branch BrPart Cover Missing -------------------------------------------------------- pricing.py 5 0 2 0 100% -------------------------------------------------------- TOTAL 5 0 2 0 100%
演習完了