-
テストのピラミッド
- ユニット > 統合 > UI
- 多くのアプリケーションは、3つの層で構成される
- ロジック > サービス > UI
- 「ユニット ↔︎ ロジック」「統合 ↔︎ サービス」「UI ↔︎ UI」というように対応している
-
UIテスト
- ○エンドツーエンド(ロジック〜UIまで全ての層を通過し、全てがつながっていることを保証する)
- ○ユーザーと同じ対象を見る
- ×高コストで遅い、壊れやすい
-
統合テスト
- UIを通過せず、サービス層をテストする。
- ○WebサービスとAPIのテスト
- ○繋がりをみる
- ×詳細さに欠ける
- 「何か」が壊れていることはわかっても、厳密に「どこ」が壊れているかまではわからない。
-
ユニットテスト
- ○超高速
- ○失敗した時にどの部分が上手くいかなかったのか厳密にわかる
- ○多目的に利用できる
- ×統合部分の確認に弱い
-
親指の法則
- UIよりもユニットテストを優先すること
- ユニットテストで埋められない部分を統合テストでカバーすること
- UIテストは限定的に使うこと
※常に、下の層でテストできないか検討すること ※すべての自動化しようとせず、過不足なく自動化すること
- 上と下の層でテストが重複する
- 上にあるテストは下にあるテストを内包しているため、部分的な重複は避けられない。
- しかし、上と下でスコープや意図が異なる
- Ex
- ユニットテスト
- スピードとフィードバックが強み。
- 開発中に重要な点についてフィードバックを得たいときに書く
- 設計を正しく反映しているか?
- 最後の変更で何かを壊していないか?
- 私たちの想定と特殊なケースはすべて整合性が取れているか?
- 新しい機能を追加することは安全か?
- UIテスト、統合テスト
- システムがきちんとつながっていることを確認。
- ユニットテスト
- Ex
- テストの意図が重複していなければ、機能面での重複は問題ない。
ユニットテスト | UIテスト |
---|---|
開発のためのもの | 検証のためのもの |
フィードバックが速い | フィードバックが遅い |
下位レベル | 上位レベル |
局所的 | エンドツーエンド |
安価 | 高価 |
実行が速い | 実行が遅い |
頑健 | 壊れやすい |
信頼できる/結果が一意に決まる | 当てにならない/結果が一意に決まらない |
開発に使う | テストに使う |
開発者視点のテスト | 顧客視点のテスト |
- UIテストはエンドツーエンドのスモークテストとして有用
- スモークテストとは、システムが基本的なレベルで稼働していることを確認する高レベルのテスト
- アプリケーションが適切にデプロイされていること
- 環境が適切に設定されていること
- アーキテクチャのすべてのパーツが正しく接続されていること
- スモークテストとは、システムが基本的なレベルで稼働していることを確認する高レベルのテスト
- キャプチャーリプレイのスクリプトよりも手で書いたテストコードの方が望ましい
- キャプチャーリプレイの欠点
- UIを少しでも変更しただけで壊れる
- 可読性が低下する
- コーディングしないので、コードを整理することができない
- キャプチャーリプレイの欠点
- テストでアサーションを入れるときは、HTMLタグを対象にする
- ページの要素を選択するには、CSSセレクタを使う(※)
- HTMLのIDが付いている要素は取得しやすい(※)
- 要素の位置に依存したUTテストを書くと、UIの変更でテストが壊れてしまう。
- Ex.)
$("input[type=text]")[0]
(jQuery)
- Ex.)
- 要素の位置に依存したUTテストを書くと、UIの変更でテストが壊れてしまう。
- (※)に対してよりよい解決案(参考)
- カスタム属性を使う
- 例えば、
data-testid
のようなカスタム属性を使って各要素を識別する。 - CSSセレクタやHTMLのIDなどは、スタイルと密結合になってしまい、変更に弱くなってしまう
- プロダクションから消したい場合は、Babelなどでコンパイル時に消すことも可能
- 例えば、
- カスタム属性を使う
- テストケース内でよく使われるURLを記述するときには、変数を使う
- UTテストを書くときは、UIとなるべく疎結合に、詳細になりすぎないように注意
- 緩く保つことがコツ
- アンチパターン例
- メッセージの内容までチェックする
- メッセージの内容はチェックせず、存在の有無をチェックする
- メッセージの内容までチェックする
- テストのピラミッドの基盤であり、他の階層に比べて多くのテストを担う
- 非常に高速に実行できるため、迅速なフィードバックを得られる
- 局所的に書くことが多く、ネットワークに接続するような処理は避けた方が良い
- モック化は、テストしたいコードに置いてアクセス困難な箇所もテストできるようにするための技術
- テストコードが十分かどうか判断するには?
- 正しく動くことを保証する
- 壊れる可能性のある箇所はすべてテストする
- よくあるエラー
- エッジケース
- 特殊な条件
- etc...
- テストファースト(TDD)
- メリット
- 必要なものだけ実装できる
- テストしやすく部品化された状態でシステムを設計、実装できる
- 実装が完了すると同時に、その動作を保証するユニットテストも出来上がっている
- メリット
- UIをテストするが、エンドツーエンドで動くわけではない(ブラウザ上での動作のみに注目)
- テストデータとしてHTMLを直接テストコードに埋め込む場合
- メリット
- テストデータとテストコードが近くに配置されるため可読性が向上する
- 必要なHTMLだけを対象にできるので、デバッグやトラブルシューティングが楽になる
- デメリット
- 実際のHTMLと乖離するリスクがある
- メリット
- UIテストよりユニットテストを優先する
- ユニットテストで埋められない隙間を統合テストでカバーする
- UIテストは限定的に使う。
- UIテストで詳細をっ見ることにコストをかけるべきではない
- 逆ピラミッドが有効の場合もある
- Ex. 巨大なレガシーシステムで、自動テストが存在しないような場合
- ユニットテストをゼロから埋めていくより、まずUIテストで対応したほうがコスパがよい
- 不安定なテストを扱うポイント
- テストを書き直す
- テストをピラミッドの下の層へ移動させる
- 価値のないテストとみなし、テストを止める
- ポイント
- スペースの入れ方
- 適切な命名
- 重複の排除
- テストの整理
- テストの対象を絞り、分離すること。一度に多くの内容をテストしようとしないこと。
- 似ているテストをコンテキストによってまとめ、頭を使わなくても理解できるようにしておくこと。
- モックのメリット
- コードの奥深くにあるオブジェクトを操作したり、監視したりできる(依存性の注入 DI)
- 実サービスを呼び出すことなく。特定の状況を準備した上でテストを実行できる
- テストの書きすぎは良くない
- テストコードとプロダクションコードの結合度が高まり、変更に弱くなってしまう。
- プライベートAPIは除き、オブジェクトのパブリックAPIだけをテストする
- モックの泥沼
- 次のような事象のこと
- 実質的なテストのコードよりも、モックの準備やエクスペクテーションの設定の行数の方がずっと多い。
- 結合されたテストが壊れて変更作業がつらいものになっていることを忘れて、ソフトウェアに変更を加えるのをためらってしまう。
- もともとのテストの意図がわからない。
- 「ポートとアダプタ」アーキテクチャ
- アプリケーションのコアな機能と、外部とのやりとりをする境界部分もしくはサービスとを分離する考え方。
- よりブラックボックステストに近いアプローチでテストを考えられる。
- メリット
- 本物のオブジェクトを使う割合が増える
- 結合度が低い
- 変更しやすい
- カバレッジが高い
- バグが少ない
- メリット
- 次のような事象のこと
- 一つのことにフォーカスするのを助けてくれる
- 開発中はいろいろな心配がでてきてノイズになってしまうことがある
- Ex. 意図しない操作、デザインの良し悪し、バグがないか、スレッドセーフか、etc...
- 開発中はいろいろな心配がでてきてノイズになってしまうことがある
- TDDびライフサイクル
- ①失敗するテストを書く
- コードの設計について考える。
- これから書こうとしているコードが正しく動作することを検証するためのテストを書く。
- コードのAPIを考えて具体化することが必要。
- ②テストを成功させる
- テストが通るコードを書く。
- ハードコードされた値を返すだけでも構わないし、少し踏み込んで圏論あ実装をしてもよい
- ③リファクタリングする
- 3ステップの中で最も重要。
- メソッドにまとめたり、変数名を変更したりする
- 美しい形でテストを通す。
- ①失敗するテストを書く
- TDDのメリット
- ①オーバーエンジニアリングを防ぐ
- 最初に失敗するテストを書いて必要性を示すまでは、プロダクションコードを追加することが許されないため
- YAGNI原則
- ②コードの設計が改善され、しっかりとテストされたコードになりやすい
- 最初から設計とテストについて考えることになるため、シンプルで読みやすいコードを生み出しやすい。
- ③複雑な内容を扱いやすい
- 一度に一つのテストだけに集中することができる
- ④単純に楽しい
- ①オーバーエンジニアリングを防ぐ
- ステップ1:失敗するテストを書く
- 設計で決めることが6つもある
- 責務:メソッドをどこに定義するか
- 命名:メソッド名、クラス名、変数名
- 入力変数:入力として何を渡すか
- 出力変数:何を返すか
- 可視性:パプリックかプライベートか
- 解決法:コードが「既に存在する」と仮定する。
- 頭を「設計モード」に切り替えられる
- 今、何を必要としているのか明らかになる
- 設計が正しいかどうかのフィードバックを、テストの形で即時に得られる
- テストしやすいコードが書ける
- 頭を「設計モード」に切り替えられる
- 設計で決めることが6つもある
- ステップ2:テストを成功させる
- テストを通すときに可能な限りシンプルな方法を選ぶようにする
- 行き詰まった場合はギアダウンする
- 今、見ているテストを通すために必要あれば、少々好ましくないコードでもコミットしてしまうということ。
- 解決方法が明らかでない場合は、躊躇わずギアダウンすること
- ステップ3:リファクタリングする
- Ex. 変数をインライン化
- 補足
- テストのコーディング作業途中で新しいテストケースを思いついた場合
- メモしておいて、取り掛かっているテストが完了したら新しいテストに着手する。
- TDDの技法は、UIテストや統合テストには向いていない
- しかし、テストファーストで考えるコンセプトはどの階層に応用しても役立つ
- ユニットテストを十分に書いても、全体がきちんと動くとは限らない
- 早い段階でUIと接続してエンドツーエンドのテストを行うべき
- TDDが向かないケース
- トランプのデッキや、ユーザーのプリレイストにある1000を超える曲をシャッフルする機能のテスト
- マルチスレッドで動作するコード
- 解決法
- 対象の機能をより小さく、よりテストしやすい断片に分類していく。
- 小さく分解してテストができたら、まとめて全体がきちんと動いているかを昔ながらの方法でテストする
- テストのコーディング作業途中で新しいテストケースを思いついた場合