継続的8章2. 8.1
導入
• 内容
– 自動受け入れテストはどのようなものなのか
– デプロイメント内でどう位置づけられるのか
3. 8.1
導入
• 基本的なCIとCDの違いは、受け入れテストス
テージ以降があるかどうからしい。
• 受け入れテストは機能テストやユニットテスト
と比べて何が違うのか?
– ストーリー・要件の受け入れ基準が満たされてい
るかを検証する
– ビジネス視点でのテスト
– 顧客が意図したことを実行できていることを証明
4. 8.2
なぜ自動受け入れテストが
欠かせないのか?
• Q. 自動受け入れテスト、作成と保守のコスト
が高いのではないか?
• A. 上手くやらないとそうなる
5. 8.2
なぜ自動受け入れテストが
欠かせないのか?
• Q. 十分なカバレッジのunit
testで事足りるは
ずだ
• A. unit
test
や
component
test
をしっかり
やっても検出できない問題があり、それを受
け入れテストで検出できる
7. 受け入れテストが得意なこと
• ユーザーシナリオのテスト
– シナリオにおいてたどる状態の欠陥は
unit
test
では検知できない
• スレッドの問題を検出することもある
• イベント駆動アプリケーションでの突発的な振
る舞いの検出
• 環境や設定ファイルに起因する問題の検出
• 大規模な変更に対するアプリケーションの保
護
8. 自動受け入れテストのコストに対する
メリット
• フィードバックループの短縮
– 修正が容易なうちに欠陥を発見できる
• テスター・開発者・顧客の協力の強制
– 皆が協力するようになる
– 皆がビジネス価値に集中する
• 良い設計の強制
– 薄いUIレイヤ
– 本番環境と開発環境で同じように実行できる設
計
9. 8.2.1
保守しやすい
受け入れテストスイートの作り方
• 受け入れ基準を分析
– ユーザーにとって価値があり、テスト可能
• 受け入れテストをレイヤ化する
13. 受け入れテストのレイヤ
受け入れ基準
Cucumber
Given…
FitNesse
When…
…
Then…
実装レイヤ
ドメインの言語を使う
UIの要素を参照しない
アプリケーションドライバレイヤ
ウィンドウドライバを含む
特定のアクションを実行して
結果を返すために、アプリケーションと
どうやってやり取りすべきか理解している
14. 受け入れ基準レイヤ
シナリオ: ユーザーの注文を口座から
正しく引き落とさなければならない
前提 ボンドという名前の証券がある
かつ デイブというユーザーがいて、口座に50ドルある
もし デイブの名義でログインする
かつ ボンドという名前の証券を選択する
かつ 4口買うことにして、それぞれ10ドルである
かつ その注文が上手くいった
ならば 口座に10ドル残る
15. 実装レイヤ
require
‘applicaDon_driver/admin_api’
require
‘applicaDon_driver/trading_ui’
…
given
/^(w+)という名前の証券がある$/
do
|instrument|
@admin_api.create_instrument(instrument)
end
given
/^(w+)という名前のユーザーがいて、口座に(w+)ドルある$/
do
|user,
amount|
@admin_api.create_user(user,
amount)
end
when
/^(w+)の名義でログインする $/
do
|user|
@trading_ui.login(user)
end
…
16. アプリケーションドライバレイヤを上手く定義すると、
受け入れ基準をテストの実装で表現できる
xUnit
などの
Unit
test
framework
受け入れ基準 兼 実装レイヤ
アプリケーションドライバレイヤ
ウィンドウドライバを含む
特定のアクションを実行して
結果を返すために、アプリケーションと
どうやってやり取りすべきか理解している
17. 実装レイヤ
public
class
PlacingAnOrderAcceptanceTest
extends
DSLTestCase
{
@Test
public
void
userOrderShouldDebitAccountCorrectly(){
adminAPI.createInstrument(“name:
bond”);
adminAPI.createUser(“Dave”,
“balance:
50.00”);
tradingUI.login(“Dave”);
tradingUI.selectInstrument(“bond”);
tradingUI.placeOrder(“price:
10.00”,
“quanDty:
4”);
tradingUI.confirmOrderSuccess(“instrument:
bond”,
“price:
10.00”,
“quanDty:
4”);
tradingUI.confirmBalance(“balance:
10.00”);
}
}
18. 実装レイヤ
ユーザー生成・登録や
証券生成は複雑な処理だが、
抽象化してシンプルに
public
class
PlacingAnOrderAcceptanceTest
extends
DSLTestCase
{
@Test
表現している
public
void
userOrderShouldDebitAccountCorrectly(){
adminAPI.createInstrument(“name:
bond”);
adminAPI.createUser(“Dave”,
“balance:
50.00”);
tradingUI.login(“Dave”);
tradingUI.selectInstrument(“bond”);
tradingUI.placeOrder(“price:
10.00”,
“quanDty:
4”);
tradingUI.confirmOrderSuccess(“instrument:
bond”,
“price:
10.00”,
“quanDty:
4”);
tradingUI.confirmBalance(“balance:
10.00”);
}
}
19. 実装レイヤ
public
class
PlacingAnOrderAcceptanceTest
extends
DSLTestCase
{
@Test
非同期処理をアプリケーションドライ
public
void
userOrderShouldDebitAccountCorrectly(){
バレイヤに押しこむことで、
adminAPI.createInstrument(“name:
bond”);
再利用可能・修正容易・最適化容易
adminAPI.createUser(“Dave”,
“balance:
50.00”);
tradingUI.login(“Dave”);
になる
tradingUI.selectInstrument(“bond”);
tradingUI.placeOrder(“price:
10.00”,
“quanDty:
4”);
tradingUI.confirmOrderSuccess(“instrument:
bond”,
“price:
10.00”,
“quanDty:
4”);
tradingUI.confirmBalance(“balance:
10.00”);
}
}
25. 8.3.2
イテレーティブな
プロジェクトにおける分析
• この節では、開発プロセスにイテレーティブな
ものを使用していることを前提とする
28. 受け入れ基準定義
• アナリスト
– 定義にほとんどの時間を使う
• これによってチームが「Done」を判断でき
るようになる
– テスターや顧客と密接に協力
• 完了を定義するために測定するべきもの
を、テスターの経験から伝えてもらえる
• 要件の本質をテスターに伝えることができ、
要件のテストに役立ててもらえる
30. 短いキックオフミーティング
• 効能
– アナリスト
• 実装やテストのコストが高い要件を作っ
てしまうことを回避できる
– テスター
• システムを誤解して、欠陥でないものを
欠陥として捉えてしまうことを防げる
– 開発者
• 関係ないものを実装してしまうことを避
けられる
31. 要件実装
• 開発者
– アナリストに相談する
• 要件のよくわからない箇所
• 要件の問題
• 要件が解決する問題に対する、効率的
な他の解決方法の提案
• デプロイメントパイプラインによって、
こうしたやり取りが促進される
32. デモ
• 開発者が作業を完了した後で行わ
れる
– 完了:=
ユニットテスト、コンポネントテ
スト、受け入れテストが通ったという
こと
• 開発者
– アナリスト、テスター、顧客の前でデ
モをする
33. デモ
• 意図したとおりに要件が実現され
ていることを確認する機会
• 細かい問題が指摘されることも多
い
– CDをやっているならばすぐに対応で
きるはず・・・
– 代替案の議論や変更のきっかけとな
る
• システムの進化の方向性について、
チームで理解を共有する機会
35. 8.3.3
実行可能な仕様としての
受け入れ基準
• 振る舞い駆動開発の考え方
– 受け入れ基準はアプリケーションの振る舞いに対
する顧客の期待という形で書かれるべき
– 受け入れテストは実行可能な仕様
• 仕様の陳腐化を防ぐ
37. 8.4.1
受け入れ基準の表現方法
• 外部DSLを使う場合(Cucumber
など)
– メリット
• 受け入れ基準がそのまま実行可能な仕様となる
– デメリット
• 実装との同期のオーバーヘッド
• 内部DSLを使う場合(xUnit
など)
– メリット
• 複雑なツールを使わない
• 開発環境の自動補完が使える
– デメリット
• ドキュメント(受け入れ基準)生成が面倒
– xUnit
テストケースからフィーチャーやストーリー、ステップをドキュメント
として生成するのは難しい
38. 8.4.2
ウィンドウドライバパターン:
テストとGUIを疎結合にする
• アプリケーションドライバのサブセット
• GUIとやりとりする
• 抽象レイヤを導入し、受け入れテストとテスト
対象システムの結合度を減らす
– GUIの変更がテストに及ぼす影響を減らしている
• あらゆるテストはこのレイヤを介してのみUIと
やりとりする
39. 8.4.2
ウィンドウドライバパターン:
テストとGUIを疎結合にする
• GUIの各部分に対して対応するデバイスドラ
イバを書かなければならない
Test
Suite
A
Test
Suite
B
Test
Suite
C
ドライバ1
ドライバ1
ドライバ1
40. レイヤリングなしの場合
@Test
Public
void
shouldDeductPaymentFromAccountBalance()
{
//
ログイン処理
selectURL(“hjp://my.test.bank.url”);
enterText(“userNameFieldID”,
“testUserName”);
enterText(“passwordFieldID”,
“testUserName”);
click(“loginBujonId”);
waitForResponse(“loginSuccessIndicator”);
//
支払い処理
…
//
asserDons
…
}
41. レイヤリングした場合
@Test
Public
void
shouldDeductPaymentFromAccountBalance()
{
AccountPanelDriver
accountPanel
=
new
AccountPanelDriver(testContext);
//
ログイン処理 <-‐
このコメントが不要となる
accountPanel.login(“testUserName”,
“testPassword”);
accountPanel.assertLoginSucceeded();
//
ステップごとにアサーションを入れても、
//
テストの意図がぼやけない。
//
Fail-‐fast
が実現された
//
支払い処理
…
//
asserDons
…
}
44. 複雑な状態にテストが依存することを
最小限に抑える
• プロダクションデータのダンプを受け入れテス
トのデータベースに入れてはならない(キャパ
シティテストは例外)
• 統制のとれた最小限のデータセットを保守す
る
– 正しく動くとわかっている開始地点を確立
• データセットをスクリプトとしてバージョン管理
• アプリケーションの公開APIを用いてセットアップ(12章)
48. テストケース間で
状態を共有する場合
• そうせざるをえない事もある
• 注意深く設計すること
– テストが脆くなりがち
– DBにレコード4件書き込み、3番目を取り出す場
合、テストの前に垂れもレコードを追加しないよう
にしなければならない
49. 開始状態を保証できない場合
• そういうこともある
• テストを防御的に作ること
– テスト開始時に状態が期待通りか確かめる
• Fail-‐fast
– 相対的な観点からテストする
• ×
array.push(‘a’)
array.size.should
be
1
– テスト開始時に
array
が空であることを保証できない
• ○
expect{
array.push(‘a’)
}.to
change
{
array.size
}.by(1)
50. 8.5.2
プロセス境界・カプセル化・テスト
• 素直なテストを書かねばならない
– 特権的なアクセスを要求しない
• テストしやすくするために設計アプローチを変
える必要が出てくる
– × バックドアを作る
• カプセル化が破壊される
– リファクタリングを妨げる
– テストが壊れやすくなる
– ○ コードをより良くモジュール化し、より良くカプ
セル化する
53. 非同期システムのテスト
特有の問題
• 問題
– テストが失敗したのか、あるいは結果を待ってい
るだけなのかわからない
• 対策
– 問題を隔離する
:=
非同期処理を同期呼び出しの
背後に隠蔽する
55. ファイル受信箱システムの
ユニットテスト
• コンポネントを別々にテストする
– テストダブルを使ってテスト対象を切り出せる
• ファイルシステムをシミュレートする
• 時計の偽物を作る
56. ファイル受信箱システムの
受け入れテスト
• ユニットテストよりも更に踏み込んだテストを
しなければならない
– デプロイメントが上手く行ったことをテスト
– ポーリングの仕組みが設定できたことをテスト
– メールサーバーが正しく設定されたことをテスト
– 全てが協調して動作することをテスト
57. ファイル受信箱システムの
受け入れテスト
• ここで問題となるのが以下の2つ
– ポーリングの間隔
– Eメールが届くまでの時間
59. 誤ったテストの実装:
メールが届く前にアサート
private
void
ConfirmEmailWasRecieved()
{
if(!EmailFound())
{
Fail(“メールを受け取れなかった”);
}
}
テストは失敗するが、
問題はプロダクトコードではなく
テストコードにある
60. 正しいテストの実装:
待つ
private
void
ConfirmEmailWasRecieved()
{
Wait(DELAY_PERIOD);
if(!EmailFound())
{
Fail(“時間内にメールを受け取れなかった”);
}
}
十分待てば、
妥当なテストとなる
61. 欠点と対策
• 欠点
– 待ち時間が容易に積み上がってしまう
• 対策
– 結果のポーリング
– 媒介となるイベントを監視
65. イベント監視
private
boolean
emailWasReceived
=
false;
private
boolean
EmailFound()
{
return
emailWasReceived;
}
public
void
EmailEventHandler(…)
{
emailWasReceived
=
true;
}
高速化という観点で、意味があるのか?
メールが大量に溜まっている場合には効果あり?
69. 外部システムと統合してしまうことの
問題点
• 受け入れテストのスコープに外部システムを
含めてしまうと、システムやその初期状態を
制御しにくくなる
• 外部システムに重大な負荷がかかる可能性
70. 妥協
• 外部システムを表現したコンポネントを作成
する
– 外部システム1つに対して1コンポネント
– 外部システムのインターフェイスを表現
• このコンポネントを、
– 自システムとのやり取りという観点
– 外部システムとの通信地点という観点
• の両方から保証する
71. 自システムとのやり取りの観点
から保証
設定
外部システムに対する
ファイル
外部システムに対する
ローカルインターフェース
ローカルインターフェース
外部システムを
外部システムとのやり取り
シミュレートする
(ゲートウェイ、アダプタ)
Test
Double
外部システムとの通信を表現する
Test
Double
を作成する
外部システム
72. 外部システムとの通信地点という観点
から保証
外部システムに対する
ローカルインターフェース
• インテグレーションテスト
– 統合点の周りに小粒のテスト
スイートを作成
外部システムとのやり取り
– 実際に外部システムと統合し
(ゲートウェイ、アダプタ)
てテストを実行する
外部システム
73. 外部システムとの通信を表現する
Test
Double
を作成する
• メリット
– テストの開始地点を構築できる
– 通信失敗のシミュレーション
– エラーが返された時のシミュレーション
– 高負荷の場合のレスポンスのシミュレーション
80. 受け入れテストのカバレッジが
不十分な場合
• 以下の3つのどれかが起こってしまう
– プロセスの最後、開発が完了したと思った頃にバ
グが見つかり時間をとられる
– 手作業で受け入れテストやリグレッションテストを
行い、大量の時間とお金を取られる
– 品質の悪いソフトウェアをリリースする
81. 自動受け入れテストは
やった方がいい
• 極端に期間の短い、人数の少ないプロジェクトで、自動受
け入れテストの実装からプロジェクトを始めるのはやりす
ぎ
– CIプロセスのステージを1つだけにし、その中でエンドツーエン
ドテストをした方がいい
• しかし、規模が大きくなれば、自動受け入れテストの価値
はコストに見合う
• 巨大なプロジェクトも小さいプロジェクトとしてスタートを切
る
• プロジェクトが大きくなると、包括的な自動受け入れテスト
を作るのは難しい
• よってどんなプロジェクトでも自動受け入れテストはやった
方がいい
82. 8.6.1
受け入れテストを
グリーンに保つ
• 受け入れテストが壊れた場合、チームは手を
止めて、問題の仕分けを行う必要がある
– テストが不安定なのか
– 環境の設定が問題なのか
– アプリケーションが変更され前提が変わったのか
– 本物の失敗なのか
• 誰かが直ちに対応し、テストが通るようにしな
ければならない
83. 通らないテストを放置すると
どうなるか
• リリースが近づくにつれ、品質を保証するために、
受け入れテストをグリーンにしようとする
• その頃には、受け入れテストの失敗の原因を特
定するのが難しくなっている
• 時間がないので、テストが削除されたり無視され
る
• 結果、作業の見積りができず、コードが実際どう
なっているのかわからないという状況に陥る
85. 受け入れテストは誰の所有物か?
• テストチームの所有物?
– 開発チームは受け入れテストへの影響を意識せ
ずに変更を行なってしまう
– テストチームが変更に気づくのは開発が終わった
後であり、その頃には開発者が別のタスクに取り
掛かっていて、テストチームはテストの修正と新
たなテストの実装で大変になる
88. デプロイメントテスト
• デプロイメントテスト
– 基盤テストや環境テストと呼ばれるスモークテスト
• 環境が期待通りに設定されることをテスト
• システムのコンポネント間の通信チャネルが正しく準
備され動作することをテスト
– デプロイメントが成功したことを確認し、後に続く
機能的な受け入れテストのための開始地点を構
築することを目的としている
91. 8.7.1
共通のタスクは
リファクタリングせよ
• テストの初期化を共通化する
– 受け入れテストはステートフル
– 受け入れテストは状態を共有しない
– よってテストの事前条件への初期化のステップを
共通化する
• 共通化された初期化のステップを効率化する
96. 8.8
まとめ
• 受け入れテストによって、
– チームメンバー全員が、ユーザーの求める価値に集中で
きる
– ソフトウェアが目的にかなっているという自信が増す
– システムを大幅に変更してもリグレッションエラーを防ぐ防
衛策ができる
– 品質が大幅に改善される
– 欠陥がいつ発生しても、フィードバックが素早く得られるの
で、直ちに修正できる
– テスターの手が開くので、テスト戦略を考えたり、実行可
能な仕様を開発したり、探索的テストやユーザーテストを
実行できる
– サイクルタイムを減少させ、継続的な開発を可能にする