境界値・組み合わせ・状態遷移のテスト
仕様から入力の範囲・境界・条件の優先順・許可された遷移を取り出し、見落としていた失敗を検出します。
説明
正常な入力で一度成功しただけでは、境界や禁止された操作まで正しく処理できるかは分かりません。入力を同じ扱いになる範囲に分ける方法、範囲の端を試す方法、条件の組合せや操作順を試す方法を使い分けます。
数量1〜4の予約受付と、draft・paid・shipped・cancelledを持つ注文処理を扱います。用意したPython実装とunittestを実行し、代表値では通る不具合を境界値で見つけ、正常な操作順では見えない遷移の誤りも確認します。run_tests.pyはunittestの結果をテスト名と失敗理由で表示します。
仕様から入力の範囲・境界・条件の優先順・許可された遷移を取り出し、見落としていた失敗を検出します。
1予約と状態の仕様
まず何を満たすべきかを読みます。受付判定は数量、受付時間、在庫の順です。注文状態は現在の状態に応じて次へ進める先が変わります。
cat spec.txt入力範囲と遷移の仕様を表示する数量が不正で受付も終了している場合はquantityを返します。条件が複数同時に成立したときの優先順も、期待結果を決めるために必要です。
2範囲の代表値だけでは通る
有効範囲の代表として2、下側の無効範囲として-1、上側として5を試します。
python3 run_tests.py test_booking.Representatives三つの範囲の代表値を試す3件ともPASSです。同じ扱いになると想定した入力群を同値クラスと呼びます。ただし代表値の成功だけで、範囲の端の実装が正しいとは限りません。
30・1・3・4・5を追加
有効範囲の端とその隣を試します。在庫と受付状態は固定し、数量だけによる違いが分かるようにしています。
python3 run_tests.py test_booking.Boundaries上下限と隣接値を試す0と4でFAILになります。0を受け入れ、4を拒否しているため、下限と上限の比較に誤りがあります。境界値分析はこのような端の取り違えを見つけるための方法です。
4比較演算子の違いを確認
修正案との差分を読みます。
diff -u数量判定の修正箇所を表示する -L booking.py -L booking-fixed.py booking.py booking-fixed.py無効な数量は1未満または4より大きい値です。修正前の0未満・4以上という条件が、仕様と一つずつずれていました。
5修正して代表値と境界を再試験
修正案を反映し、代表値と境界値の両方を実行します。
cp booking-fixed.py booking.py数量の条件を修正する
python3 run_tests.py test_booking.Representatives test_booking.Boundaries既存と追加のケースを実行する8件が成功します。追加したケースだけでなく、以前から通っていた入力も確認し、変更の影響を調べています。
6条件の組合せを表にする
数量が正しいか、受付中か、在庫が足りるかを決定表で表します。anyは、その行の結果には影響しない条件です。
cat decisions.csv条件と結果の対応を読む
python3 run_tests.py test_booking.Decisions組合せと優先順を試す数量が不正なら他条件にかかわらずquantity、受付終了なら在庫にかかわらずclosedです。在庫が数量と等しい場合はacceptedになります。条件を単独で試すだけでなく、優先順と境界の組合せも確認しています。
7正常な注文の流れは成功する
注文をdraftからpaid、shippedへ進める試験を実行します。
python3 run_tests.py test_order.HappyPath支払後に出荷する正常経路を試す正常な経路は通ります。しかし支払を飛ばした出荷や、出荷後の取消しが拒否されるかは、この試験では調べていません。
8禁止された遷移を実行する
未払いで出荷、出荷後に取消し、取消し後に支払という三つを試します。期待するのはValueErrorと元の状態の保持です。
python3 run_tests.py test_order.Forbidden禁止経路と失敗後の状態を試す3件とも例外が発生せずFAILになります。目標状態の名前が正しいかを調べるだけでは、現在の状態から移ってよいかを判定できません。
9現在の状態ごとに次の候補を決める
修正案では、各状態から進める集合を定義しています。出荷済みと取消し済みの集合は空です。
diff -u遷移判定の変更を読む -L order.py -L order-fixed.py order.py order-fixed.py
cp order-fixed.py order.py現在状態に応じた判定へ置き換える
python3 run_tests.py test_order.HappyPath test_order.Forbidden正常経路と禁止経路を再試験する4件が成功します。正常経路が保たれ、拒否した操作によって状態が変わらないことも確認できました。状態遷移の試験では、入力だけでなく操作前の状態と操作後の状態を対応させます。
まとめ
同値クラスの代表値と、範囲の端を狙う境界値は補い合います。
決定表では条件の組合せと優先順を明らかにし、入力が重なったときの期待結果を決めます。
状態遷移では正常な経路に加え、禁止された操作と拒否後に残る状態も確認します。
操作履歴と解説を振り返れます。
シェル / Python 3
cat spec.txt予約数量は整数の1〜4。範囲外ならquantityを返す。 数量が正しくても受付終了ならclosedを返す。 受付中でも在庫が数量未満ならstockを返す。 すべてを満たせばacceptedを返す。 注文状態: draft→paid→shipped。draftまたはpaidからcancelledへ進める。 shippedとcancelledは終端状態で、その後の遷移は拒否する。
python3 run_tests.py test_booking.RepresentativesPASS test_booking.Representatives.test_middle PASS test_booking.Representatives.test_negative PASS test_booking.Representatives.test_over tests=3 failures=0 errors=0
python3 run_tests.py test_booking.BoundariesPASS test_booking.Boundaries.test_five FAIL test_booking.Boundaries.test_four: 'quantity' != 'accepted' - quantity + accepted PASS test_booking.Boundaries.test_one PASS test_booking.Boundaries.test_three FAIL test_booking.Boundaries.test_zero: 'accepted' != 'quantity' - accepted + quantity tests=5 failures=2 errors=0
diff -u -L booking.py -L booking-fixed.py booking.py booking-fixed.py--- booking.py
+++ booking-fixed.py
@@ -1,5 +1,5 @@
def reserve(quantity,stock,is_open):
- if type(quantity) is not int or quantity < 0 or quantity >= 4:
+ if type(quantity) is not int or quantity < 1 or quantity > 4:
return 'quantity'
if not is_open:
return 'closed'cp booking-fixed.py booking.py
python3 run_tests.py test_booking.Representatives test_booking.BoundariesPASS test_booking.Representatives.test_middle PASS test_booking.Representatives.test_negative PASS test_booking.Representatives.test_over PASS test_booking.Boundaries.test_five PASS test_booking.Boundaries.test_four PASS test_booking.Boundaries.test_one PASS test_booking.Boundaries.test_three PASS test_booking.Boundaries.test_zero tests=8 failures=0 errors=0
cat decisions.csv
python3 run_tests.py test_booking.Decisionsquantity_valid,is_open,stock_enough,result false,any,any,quantity true,false,any,closed true,true,false,stock true,true,true,accepted PASS test_booking.Decisions.test_closed_even_without_stock PASS test_booking.Decisions.test_invalid_even_if_closed PASS test_booking.Decisions.test_stock_equal PASS test_booking.Decisions.test_stock_short tests=4 failures=0 errors=0
python3 run_tests.py test_order.HappyPathPASS test_order.HappyPath.test_paid_then_shipped tests=1 failures=0 errors=0
python3 run_tests.py test_order.ForbiddenFAIL test_order.Forbidden.test_cancel_after_shipping: ValueError not raised FAIL test_order.Forbidden.test_reopen_cancelled: ValueError not raised FAIL test_order.Forbidden.test_skip_payment: ValueError not raised tests=3 failures=3 errors=0
diff -u -L order.py -L order-fixed.py order.py order-fixed.py
cp order-fixed.py order.py
python3 run_tests.py test_order.HappyPath test_order.Forbidden--- order.py
+++ order-fixed.py
@@ -1,7 +1,8 @@
class Order:
def __init__(self):self.state='draft'
def move(self,target):
- if target not in {'paid','shipped','cancelled'}:
+ allowed={'draft':{'paid','cancelled'},'paid':{'shipped','cancelled'},'shipped':set(),'cancelled':set()}
+ if target not in allowed[self.state]:
raise ValueError('transition rejected')
self.state=target
return self.state
PASS test_order.HappyPath.test_paid_then_shipped
PASS test_order.Forbidden.test_cancel_after_shipping
PASS test_order.Forbidden.test_reopen_cancelled
PASS test_order.Forbidden.test_skip_payment
tests=4 failures=0 errors=0演習完了