一覧SE-05-2 · テストの設計

単体テスト・結合テスト・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

1
cat scope.txt
Unit: service.place → MemoryStore。実SQLとCLI引数は通らない。
Integration: service.place → SqlStore → SQLite。CLI引数は通らない。
EndToEnd: CLI引数 → service.place → SqlStore → SQLite → 標準出力。
この演習のシステム境界はCLIアプリ。ブラウザや外部決済は境界に含めない。
2
cat service.py cat fake.py python3 run_tests.py test_layers.Unit
def 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=0
3
python3 run_tests.py test_layers.Integration
FAIL test_layers.Integration.test_saved_values: 3 != 200
tests=1 failures=1 errors=0
4
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=0
5
python3 cli.py 3 200
quantity=2 unit_price=200 total=400
6
python3 run_tests.py test_layers.EndToEnd
FAIL 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
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
--- 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=600
8
python3 run_tests.py test_layers.Unit test_layers.Integration test_layers.EndToEnd
PASS 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

演習完了