単体テスト・結合テスト・E2Eテスト
テストの範囲と置き換えた依存を把握し、成功した検査が何を保証しているか説明します。
説明
テストが成功したときは、何を実際に通したのかを確認する必要があります。保存先をメモリ内の代用品に替えたテストでは、実SQLの列の対応やCLIの引数処理は確かめられません。
数量3、単価200円の注文を保存し、合計600円を表示する小さなCLIアプリを使います。単体試験、SQLiteを使う結合試験、CLIから標準出力までの試験を順に実行し、それぞれが検出する別の不具合を直します。
テストの範囲と置き換えた依存を把握し、成功した検査が何を保証しているか説明します。
1各試験が通る範囲
試験範囲の一覧を読みます。この演習の完成したシステムは、CLIから引数を受けて注文を保存し、結果を出力するアプリです。
cat scope.txt入口・依存・出力の範囲を確認する単体では保存先をMemoryStoreに置き換え、結合では実際のSQLiteを使います。EndToEndはCLIの入口と出力まで含みます。これらの名前だけで範囲が決まるわけではなく、対象システムの境界を先に定めます。
2メモリ内の代用品で単体試験
serviceとMemoryStoreを読み、単体試験を実行します。代用品も実際に値を保持する小さな実装です。
cat service.py
cat fake.py代用品が保存する内容を読む
python3 run_tests.py test_layers.Unitメモリ保存を使う単体試験を実行する数量3・単価200が保存され、単体試験は成功します。MemoryStoreはSQLを使わないため、SQLのプレースホルダーへ渡す値の誤りは、この成功からは分かりません。
3実SQLiteとの結合では失敗する
保存先をSqlStoreにした結合試験を実行します。入力と期待値は同じ単価200円です。
python3 run_tests.py test_layers.Integration実SQLiteへ保存して読み戻す保存後の単価が3になり、期待する200と一致しません。代用品では通った処理でも、実際の保存実装との間で誤りが起きています。
4SQLに渡す値の対応を直す
保存処理の修正案との差分を確認します。三つ目の列はunit_priceなのに、数量を二度渡していました。
diff -uSQLパラメーターの変更箇所 -L store.py -L store-fixed.py store.py store-fixed.py
cp store-fixed.py store.py単価を正しい引数へ直す
python3 run_tests.py test_layers.Unit test_layers.Integration二つの範囲で再試験する単体と結合の2件が成功します。保存後の値まで確認したため、SQLがエラーなく実行できるだけでは見つからない列対応の誤りを検出できました。
5CLIから実行してみる
CLIへ数量3、単価200を渡します。サービスと保存処理の試験が成功した後でも、入口の処理は別に残っています。
python3 cli.py 3 200CLIへ数量3と単価200を入力する数量2・単価200・合計400が表示されます。料金計算の期待値600とは異なりますが、原因はSQLではなくCLIから渡される数量です。
6入口から出力までを試験する
CLIを別プロセスで実行し、標準出力を比較するテストを動かします。
python3 run_tests.py test_layers.EndToEndCLI引数から標準出力までを試す引数3が処理へ渡らず、固定値2が使われているためFAILです。単体・結合テストはserviceへ直接3を渡していたので、この問題を通っていませんでした。
7引数の読取りを修正する
数量もコマンド引数から読む修正を確認し、反映します。
diff -u固定数量を引数読取りへ変える差分 -L cli.py -L cli-fixed.py cli.py cli-fixed.py
cp cli-fixed.py cli.py入口を修正する
python3 cli.py 3 200同じ入力で表示を確認する数量3・単価200・合計600になりました。CLIの変換、業務処理、保存、読戻し、出力がこの入力でつながっています。
8三つの試験をまとめて再実行
最後に三つの範囲を再試験します。
python3 run_tests.py test_layers.Unit test_layers.Integration test_layers.EndToEnd範囲の異なる三つの試験を実行する3件が成功します。EndToEndを1件通しただけで全入力の動作が証明されるわけではありません。小さい範囲で多くの条件を試し、境界の接続と利用の流れを別の試験で補う設計ができます。
まとめ
代用品を使う単体試験は、置き換えた実装内部を検査しません。実SQLとの値の対応は結合試験で確認しました。
CLIの引数処理は、サービスを直接呼ぶ試験では通りません。入口からの試験で固定値2の不具合を検出しました。
成功したテストの名前だけで判断せず、通った入口・依存・出力と、試していない条件を確認します。
操作履歴と解説を振り返れます。
シェル / Python 3
cat scope.txtUnit: service.place → MemoryStore。実SQLとCLI引数は通らない。 Integration: service.place → SqlStore → SQLite。CLI引数は通らない。 EndToEnd: CLI引数 → service.place → SqlStore → SQLite → 標準出力。 この演習のシステム境界はCLIアプリ。ブラウザや外部決済は境界に含めない。
cat service.py
cat fake.py
python3 run_tests.py test_layers.Unitdef place(store,quantity,unit_price):
order={'id':1,'quantity':quantity,'unit_price':unit_price}
store.save(order)
return store.get(1)
class MemoryStore:
def __init__(self):self.rows={}
def save(self,order):self.rows[order['id']]=dict(order)
def get(self,key):return dict(self.rows[key])
PASS test_layers.Unit.test_order_values
tests=1 failures=0 errors=0python3 run_tests.py test_layers.IntegrationFAIL test_layers.Integration.test_saved_values: 3 != 200 tests=1 failures=1 errors=0
diff -u -L store.py -L store-fixed.py store.py store-fixed.py
cp store-fixed.py store.py
python3 run_tests.py test_layers.Unit test_layers.Integration--- store.py
+++ store-fixed.py
@@ -4,7 +4,7 @@
self.db=sqlite3.connect(':memory:')
self.db.execute('CREATE TABLE orders(id INTEGER PRIMARY KEY,quantity INTEGER,unit_price INTEGER)')
def save(self,order):
- self.db.execute('INSERT INTO orders VALUES(?,?,?)',(order['id'],order['quantity'],order['quantity']))
+ self.db.execute('INSERT INTO orders VALUES(?,?,?)',(order['id'],order['quantity'],order['unit_price']))
def get(self,key):
row=self.db.execute('SELECT id,quantity,unit_price FROM orders WHERE id=?',(key,)).fetchone()
return dict(zip(('id','quantity','unit_price'),row))
PASS test_layers.Unit.test_order_values
PASS test_layers.Integration.test_saved_values
tests=2 failures=0 errors=0python3 cli.py 3 200quantity=2 unit_price=200 total=400
python3 run_tests.py test_layers.EndToEndFAIL test_layers.EndToEnd.test_cli: 'quantity=2 unit_price=200 total=400' != 'quantity=3 unit_price=200 total=600' - quantity=2 unit_price=200 total=400 ? ^ ^ + quantity=3 unit_price=200 total=600 ? ^ ^ tests=1 failures=1 errors=0
diff -u -L cli.py -L cli-fixed.py cli.py cli-fixed.py
cp cli-fixed.py cli.py
python3 cli.py 3 200--- cli.py
+++ cli-fixed.py
@@ -3,6 +3,6 @@
from store import SqlStore
store=SqlStore()
try:
- order=place(store,2,int(sys.argv[2]))
+ order=place(store,int(sys.argv[1]),int(sys.argv[2]))
print(f"quantity={order['quantity']} unit_price={order['unit_price']} total={order['quantity']*order['unit_price']}")
finally:store.close()
quantity=3 unit_price=200 total=600python3 run_tests.py test_layers.Unit test_layers.Integration test_layers.EndToEndPASS test_layers.Unit.test_order_values PASS test_layers.Integration.test_saved_values PASS test_layers.EndToEnd.test_cli tests=3 failures=0 errors=0
演習完了