一覧SE-06-1 · 品質測定とレビュー

カバレッジとアサーション

実行範囲の指標と不具合の検出を区別し、カバレッジが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 -m

3件が成功し、分岐を含むカバレッジも100%です。ただし試した金額は一部です。高いカバレッジと期待値の検査を組み合わせ、仕様上の境界や異常条件も別に検討します。

まとめ

行カバレッジ100%でも、条件の偽側を通っていない場合があります。

行と分岐が100%でも、型しか検査しない試験は誤った金額を見逃します。

変異を検出できるかと、期待値が要求を表しているかを確認し、実行範囲だけで品質を判断しません。

操作履歴と解説を振り返れます。

シェル / Python 3

1
cat pricing.py cat one.py
def price(amount,member):
    result=amount
    if member:
        result=amount*90//100
    return result
from pricing import price
assert isinstance(price(100,True),int)
2
python3 -m coverage run --source=pricing one.py python3 -m coverage report -m
Name         Stmts   Miss  Cover   Missing
------------------------------------------
pricing.py       5      0   100%
------------------------------------------
TOTAL            5      0   100%
3
python3 -m coverage run --branch --source=pricing one.py python3 -m coverage report -m
Name         Stmts   Miss Branch BrPart  Cover   Missing
--------------------------------------------------------
pricing.py       5      0      2      1    86%   3->5
--------------------------------------------------------
TOTAL            5      0      2      1    86%
4
cat both.py python3 -m coverage run --branch --source=pricing both.py python3 -m coverage report -m
from 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%
5
cp pricing-mutant.py pricing.py python3 -m coverage run --branch --source=pricing both.py python3 -m coverage report -m
Name         Stmts   Miss Branch BrPart  Cover   Missing
--------------------------------------------------------
pricing.py       5      0      2      0   100%
--------------------------------------------------------
TOTAL            5      0      2      0   100%
6
python3 -c "from pricing import price; print(price(100,True),price(100,False))"
80 100
7
python3 run_tests.py test_pricing.Values
FAIL 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
8
cp pricing-original.py pricing.py python3 -m coverage run --branch --source=pricing run_tests.py test_pricing.Values python3 -m coverage report -m
PASS 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%

演習完了