テストの独立性と再現性
前の実行に依存しない準備と後片付けを行い、修正前に失敗し修正後に成功する回帰テストを残します。
説明
テストが前のテストの設定に頼っていると、まとめて実行した場合だけ成功し、単独実行や順序変更で失敗することがあります。また、不具合を直すときは、その不具合を再現する入力をテストとして残す必要があります。
値引率を共有するPythonコードで、実行順による成功・失敗の違いを確認します。各テストが設定を用意し、終了後に戻す形へ直した後、101円の10%引きで丸め方を間違える不具合を回帰テストに追加します。
前の実行に依存しない準備と後片付けを行い、修正前に失敗し修正後に成功する回帰テストを残します。
1共有設定を使う二つの試験
test_configureは値引率10を設定しますが、test_discountは自分では設定しません。初期値は0です。
cat settings.py初期の値引率を読む
cat test_shared.py各テストが準備する状態を読むtest_discountが期待する180円には、値引率10が必要です。別のテストがその状態を残すことに依存している構造になっています。
2設定する試験を先にすると通る
test_configure、test_discountの順に同じプロセスで実行します。
python3 run_tests.py test_shared.Shared.test_configure test_shared.Shared.test_discount設定するテストを先に実行する2件とも成功します。しかし後半は前半が残した共有設定を利用しているので、この成功だけでは独立性を確認できません。
3単独では再現条件が欠ける
test_discountだけを新しいプロセスで実行します。
python3 run_tests.py test_shared.Shared.test_discount前のテストなしで単独実行する実際の200円と期待する180円が不一致になります。初期の値引率は0なので、値引きされません。テスト自身が必要な設定を用意していなかったことが原因です。
4逆順でも失敗する
二つのテストの順序を逆にします。
python3 run_tests.py test_shared.Shared.test_discount test_shared.Shared.test_configure逆順に実行する先頭のtest_discountが失敗します。実行順を固定して見えなくするのではなく、各テストの開始条件をそろえる必要があります。
5各テストで設定し、後で戻す
独立した版ではsetUpが毎回値引率を10にし、addCleanupで元の値へ戻します。
cat test_isolated.py準備と後片付けを読む
python3 run_tests.py test_isolated.Isolated.test_discount単独でも必要な状態がそろうことを確認する
python3 run_tests.py test_isolated.Isolated.test_discount test_isolated.Isolated.test_configure単独と逆順の両方が成功します。addCleanupはテストが失敗した場合にも呼ばれる後片付けを登録できます。共有状態がある場合は、自分に必要な値を用意し、次の試験へ余計な状態を残さないようにします。
6金額の丸め方の不具合を読む
次は具体的な不具合の再現条件です。仕様では、値引き後の額に端数があれば切り捨てます。
cat bug.txt入力・期待結果・原因を読む
python3 run_tests.py test_rounding.Existing既存の金額ケースを実行する既存の100円と200円の試験は成功します。どちらも10%引きの結果が整数なので、切り捨てる順番の違いが表れません。
7101円の再現ケースを追加する
101円の10%引きが90円になることを確認します。値引きなしの条件も加えています。
python3 run_tests.py test_rounding.Regression不具合を再現する入力と期待値を試す101円のケースが91円になり、FAILします。修正前のコードで失敗することを確かめると、このテストが対象の不具合を実際に検出することが分かります。
8丸める順番を直して全件再実行
値引額を先に切り捨てる計算から、値引き後の額を切り捨てる計算へ変更します。
diff -u切り捨ての位置の違いを確認する -L rounding.py -L rounding-fixed.py rounding.py rounding-fixed.py
cp rounding-fixed.py rounding.py計算を修正する
python3 run_tests.py test_rounding.Existing test_rounding.Regression既存と回帰のケースを再実行する4件が成功します。修正だけで終わらず、101円のケースを残すことで、後の変更で同じ計算へ戻ってしまった場合も検出できます。
まとめ
テストは、単独実行や順序変更でも必要な開始状態を自分で用意できるようにします。
共有設定は後片付けで元へ戻し、ほかのテストへ影響を残さないようにします。
回帰テストでは具体的な再現入力を残し、修正前の失敗と修正後の成功を確認します。
操作履歴と解説を振り返れます。
シェル / Python 3
cat settings.py
cat test_shared.pyrate=0
import unittest,settings
from quote import total
class Shared(unittest.TestCase):
def test_configure(self):
settings.rate=10
self.assertEqual(total(100),90)
def test_discount(self):
self.assertEqual(total(200),180)python3 run_tests.py test_shared.Shared.test_configure test_shared.Shared.test_discountPASS test_shared.Shared.test_configure PASS test_shared.Shared.test_discount tests=2 failures=0 errors=0
python3 run_tests.py test_shared.Shared.test_discountFAIL test_shared.Shared.test_discount: 200 != 180 tests=1 failures=1 errors=0
python3 run_tests.py test_shared.Shared.test_discount test_shared.Shared.test_configureFAIL test_shared.Shared.test_discount: 200 != 180 PASS test_shared.Shared.test_configure tests=2 failures=1 errors=0
cat test_isolated.py
python3 run_tests.py test_isolated.Isolated.test_discount
python3 run_tests.py test_isolated.Isolated.test_discount test_isolated.Isolated.test_configureimport unittest,settings
from quote import total
class Isolated(unittest.TestCase):
def setUp(self):
before=settings.rate
self.addCleanup(setattr,settings,'rate',before)
settings.rate=10
def test_configure(self):self.assertEqual(total(100),90)
def test_discount(self):self.assertEqual(total(200),180)
PASS test_isolated.Isolated.test_discount
tests=1 failures=0 errors=0
PASS test_isolated.Isolated.test_discount
PASS test_isolated.Isolated.test_configure
tests=2 failures=0 errors=0cat bug.txt
python3 run_tests.py test_rounding.ExistingBUG-17: 101円の10%引きが91円になる。 合意した計算: 値引き後の額を計算し、1円未満を切り捨てる。 101 × 90 / 100 = 90.9円 → 90円。 誤実装: 値引き額10.1円を先に10円へ切り捨て、101-10=91円。 PASS test_rounding.Existing.test_hundred PASS test_rounding.Existing.test_two_hundred tests=2 failures=0 errors=0
python3 run_tests.py test_rounding.RegressionFAIL test_rounding.Regression.test_101_yen: 91 != 90 PASS test_rounding.Regression.test_no_discount tests=2 failures=1 errors=0
diff -u -L rounding.py -L rounding-fixed.py rounding.py rounding-fixed.py
cp rounding-fixed.py rounding.py
python3 run_tests.py test_rounding.Existing test_rounding.Regression--- rounding.py +++ rounding-fixed.py @@ -1,2 +1,2 @@ def total(amount,rate): - return amount - amount*rate//100 + return amount*(100-rate)//100 PASS test_rounding.Existing.test_hundred PASS test_rounding.Existing.test_two_hundred PASS test_rounding.Regression.test_101_yen PASS test_rounding.Regression.test_no_discount tests=4 failures=0 errors=0
演習完了