テスト 思考垂れ流し
テストケース設計:テストケース量とリスクのバランス
テストケース設計:引数を属性に分解する
todo.icon テストケース設計:事前属性の組み合わせに拘らず、条件を抽象化した組み合わせもあり
参考:テスト 思考垂れ流し#690de2bc0000000000d4dcb0
テストケース設計:同値クラス分割
テストケース設計:テストケース作成の基本フロー
テストケース設計:原則、テストケースは仕様(期待される振る舞い)に基づいて設計する
参考:
BDD
https://tidyfirst.substack.com/p/canon-tdd
テストケース設計:とは言え、内部実装のテストケースも作る時もある
テストケース設計:想定のエラーも仕様の一部である
テスト指針:カバレッジ分析は「テストしてないコードを見つける」だけに過ぎず、それ以上の意味はない
テスト指針:モックはなるべく使わない(古典派)
参考:
https://martinfowler.com/bliki/UnitTest.html
https://www.signadot.com/blog/why-developers-shouldnt-write-mocks-a-guide-to-modern-testing
テストケース練習:シンプル純粋関数①
テストケース練習:シンプル純粋関数②
テストケース練習:シンプル純粋関数③
テストケース練習:複雑な純粋関数①
テストケース練習:複雑な純粋関数②
テストケース練習:番外編①
テストケース練習:シンプルな非純粋関数①
テストケース練習:シンプルな非純粋関数②
関数設計:責任を委譲してテストしやすいコードにする
【読書】実践テスト駆動開発
【読書】テスト駆動開発
メモ
hr.icon
1. テスト対象の視点
内部 vs 外形(ホワイトボックス vs ブラックボックス)
純粋関数 vs 非純粋関数(副作用の有無)
2. テストの実装方法
モック vs 非モック(依存関係の扱い)
DB vs モック(永続層のテスト方法)
モックの完全性: 最小限 vs 完全実装
3. テストの範囲と戦略
ピラミッド型: ユニット(多)→ 統合(中)→ E2E(少)
スモークテスト vs 詳細テスト(浅く広く vs 深く狭く)
リグレッションテスト: 既存機能の保護
追加すべき重要な軸
4. テストの実行タイミング
プリコミット vs ポストコミット
ローカル vs CI/CD vs デプロイ前 vs 本番監視
継続的実行 vs オンデマンド
5. テストの自動化レベル
完全自動 vs 半自動 vs 手動
記録再生型 vs スクリプト型
自己修復テスト vs メンテナンス必要
6. テストデータの戦略
固定データ vs ランダム生成 vs プロパティベース
本番データ(サニタイズ)vs 合成データ
データ駆動テスト vs ハードコード
インメモリ vs 永続化
7. テストの独立性
完全独立 vs 順序依存
並列実行可能 vs 順次実行必須
ステートレス vs ステートフル
テストごとのセットアップ vs 共有フィクスチャ
8. カバレッジの基準
コードカバレッジ: 行 vs 分岐 vs 条件 vs パス
境界値分析 vs 同値分割 vs ペアワイズ
変異テスト(ミューテーションテスト)
リスクベースのカバレッジ
9. テストの粒度(詳細版)
ユニット(関数・クラス)
コンポーネント(複数クラス)
統合(サブシステム間)
システム(全体)
エンドツーエンド(ユーザー視点)
10. 非機能要件のテスト
パフォーマンステスト: 負荷 vs ストレス vs スパイク
セキュリティテスト: 脆弱性スキャン vs ペネトレーション
ユーザビリティテスト
互換性テスト(ブラウザ、OS、デバイス)
回復性テスト(カオスエンジニアリング)
11. テスト作成のアプローチ
TDD(テスト駆動開発)vs テスト後書き
BDD(振る舞い駆動開発)
ATDD(受け入れテスト駆動開発)
探索的テスト vs スクリプトテスト
12. テストの実行環境
ローカル開発環境
専用テスト環境(ステージング)
本番環境(カナリア、A/Bテスト)
コンテナ vs VM vs ベアメタル
クラウド vs オンプレミス
13. エラー検出の段階
静的解析(リント、型チェック)
コンパイル時
実行時(ユニットテスト)
統合時
本番監視
14. テストの保守性
フレジャイル(壊れやすい)vs ロバスト(頑健)
実装依存 vs インターフェース依存
DRY原則の適用度
テストコードの可読性
15. 契約テスト
プロバイダー契約 vs コンシューマー契約
スキーマ検証
API契約テスト
16. スナップショットテスト
UI スナップショット
データスナップショット
承認テスト
17. テストの範囲(レイヤー別)
フロントエンド: コンポーネント vs ビジュアルリグレッション
API: 契約 vs 統合
バックエンド: ビジネスロジック vs データアクセス
インフラ: 構成テスト
18. テストの戦略パターン
テストピラミッド(従来型)
テストトロフィー(統合テスト重視)
テストダイヤモンド(統合とE2E重視)
アイスクリームコーン(アンチパターン:E2E過多)
19. テストの目的
検証(Verification): 正しく作られているか
妥当性確認(Validation): 正しいものを作っているか
ドキュメント化
設計の改善
20. テストの範囲決定
リスクベース: 重要度・影響度で優先順位
頻度ベース: よく使う機能を重点的に
網羅的: すべてをテスト
サンプリング: 代表的なケースのみ
テストとは...
大小関わらず対象の部品が仕様・期待通りに動くかを検品するためのもの
なので...
仕様に抜け漏れやミスがあるなら、テストが完璧でも不完全である
テストケースに関しては...
基本的には、部品の実装を見ずに仕様だけを見てテストケースを作るべし
ポイント
hr.icon
テスト界隈にはモックを積極的に使う派(ロンドン学派)と使わない派(デトロイト学派、古典派)がある。
ロンドン学派:
テスト対象の依存関係は、不変的なもの(Enum, Const)以外、全てモックに置き換える
「単体」の定義は、クラスを指すのであり、単体クラスで完結できないテストは全て統合テストと捉える。
コードを検証する。(ホワイトボックステストみたいな感じかな?onigiri.w2.icon)
動作の内部の隅から隅までチェック。
デトロイト学派(古典派):
テスト対象の依存関係は、共有依存関係だけモックにする
共有依存関係 = ユニットテスト間で状態が共有されるもの
例えば、データベースなんかは、モックしない場合ユニットテスト間で状態を共有することになりやすい。
共有せずに毎ユニットテストごとに初期化するっていう手もあるが、デメリットと比較して選択すべき。
「単体」の定義は振る舞い。つまり、同時に複数のクラスがまたがってもいい。(モックしないし)
動作(振る舞い)を検証する。
用意されたメニュー・契約を従ってるかどうかだけチェック。
2つ以上の動作を検証する場合は「統合テスト」として扱う。
参考
ユニットテストの流派
モックはスタブではない
デトロイト派閥でやるなら、クリーンアーキテクチャの内側からテストするのが良さげ
デトロイト派の方針としては、「ドメインモデルから実装及びテストを書いていき、レイヤーを重ねていくアプローチ」を取る。
クリーンアーキテクチャではドメインモデルが一番中核にあり、それに依存する形で外側のレイヤーが重なっていく。
あの依存関係のまま、外側にテストを広げていく感じ。
で、外側でのテストの際には、内の実装コードをモックせずに利用するのが原則。
私もこっちの方法に賛成するonigiri.w2.icon
1. モック作るのしんどい
2. モックによってメインコードの変更検知ができなくなる可能性あり
モックは必要悪で、しないにこしたことはない - blog.8-p.info
モックの一番の問題は、本番とテストで違うコードが走ることで、これは自動テストの価値をだいぶ下げてしまう。テストが通っているのは、コードが正しいのか、コードがモックと揃っているからなのかわからなくなる。
もう一つの問題は、モックと実装が密結合してしまうことで、後々コードを変更するのが大変になる。実装が変えやすいようにテストを書くのに、実装を変えづらくなっては本末転倒だ。
デトロイト派のデメリットもしっかり認識しておくこと
レイヤが重なっていって結合数が増えた場合、テスト失敗の際にどこが原因かを見つけにくい可能性がある
あれ、これしか思いつかないな...orz
まあ、他にも外部システムに依存する場合、環境によって動作が変わる可能性があるとかありそう。
だけど...そこは「外部プロセス依存」みたいに捉えられそうであり、「モック化する」か「統合テストに任せる」になりそうだね。
統合テストの場合、本番と同じ環境でテストするイメージなので、環境が変わるってことはないやろし。
基本的なテストと実装の時系列は以下が良いんじゃないかなと
時系列案
1. 外部から利用される際のタッチポイント(ex: API、ユースケース、...)を洗い出し、それらのテストケースを作成
これはできるなら、コードで先に作成しておくのが良いかと
なお、タッチポイントとかは、事前設計の段階で粗方固まっていると想定。
もちろん実装の最中に変更がある可能性もあるが、それは仕方ない。
2. クリーンアーキテクチャの中心部分から、TDDを使って実装していく
テストケース作成 -> まずはRed -> 次にメインをテストに合わせる形で実装してGreen -> その後にメインコードをちゃんと書いてRed, Greenを調整 -> 最後はAll Greenにする。
point.icon ただし、全部が全部、単体テスト書かなくても良いかなぁって思う
-> テスト 思考垂れ流し#65017692b3641f0000929927
3. 外部システムはモックにし内部のタプルは全て実物を使った上で、「1.」のテストを実装して実行
4. All Greenになるよう修正
5. 外部システムも全て実物にした上で「1.」のユースケースをテスト?
6. All Greenになるよう修正
所感
3, 5は一緒でも良い可能性ある。つまり、モックを用意しない。
3はいわゆる「内部結合テスト」で、5は「外部結合テスト」ってやつだな。
テストは実装の動作品質だけじゃなく、「セキュリティ」「パフォーマンス」っていう観点もあるはず。
ここら辺はどこでテストするんだろうな。
単体テストをする/しないの判断
単体テストの考え方/使い方 まとめ
table:
協力オブジェクトの数: 少ない 協力オブジェクトの数: 多い
コードの複雑さ/ドメインにおける重要性: 低い 取るにとらないコード コントローラ
コードの複雑さ/ドメインにおける重要性: 高い ドメインモデル・アルゴリズム 過度に複雑なコード
取るに足りないコード: 単体テストしなくていい
ドメインモデル・アルゴリズム:単体テスト頑張ろう
コントローラ(API関数などだな):統合テストに譲ろう
これユースケースも当たるのかな?
過度に複雑なコード:設計が危うい。分割できないか考えなおせ。
コントローラーが複雑になるのは最悪仕方ないって意見もあった。
ただ、ビジネスロジックがコントローラーに含まれるのだけは避けたいな
それすると、コントローラーが重要なテスト対象になってしまう。
つまり、単体テストの必要性が出てくる。それはしんどいことになりそう。
TDDはテスト手法ではなく生産性向上の手法と考えている
テスト駆動開発、テスト駆動開発 指針
単体テストを書いても意味ないっていう意見もあるけど、自分はTDDがあるなら意味ありそうって思う。
コードを書く際に仕様を意識せずに書くと右往左往して開発スピードが遅くなる。自分場合は。。。
コードに自信が持てないし、何を作れば良いんだっけ?ってなる時もある。
だから、先に仕様としてテストケースを洗い出しつつ、All Greenになるように開発するのがスピード高そう。
けど、それがスピード遅いってんなら多分必要ないかもしれない。統合テストを重視した方がいいということもあるかも。
統合テストを重視した方が品質は上がりやすいとかなんとか
テストトロフィーって考え方があるんだって。
Testing Trophyとは〜フロントエンドテストについて学んでみた〜 – 株式会社ライトコード
もはや、システムが小規模なら多分統合テストだけで良いんじゃない?
品質も賄えるし、開発速度も上がるよ多分。
統合テストの結果がGreenになるまで、リファクタリングや修正を繰り返せばOKってだけよ。
小規模ならそれで間に合いそうって思う。
統合テスト時の観点
統合テストの定義
1つのシステム(プロセス)で決められたユースケースとそれに付随する挙動が期待された通りになるかテスト
外部結合、内部結合っていう分け方があるにはありそう。
外部結合 = 外部システム/プロセスはモックで代替
内部結合 = 外部システムは本物使う
以下、観点
1. 入力/出力に関連するテスト
全ての正常入力に対して正常レスポンスが返るか
異常入力に対して異常レスポンスが返るか
2. 副作用(外部システム/プロセス)に関連するテスト
正常入力に対して正しい副作用(データ保存、外部API叩くなど)がされるか
異常入力に対して正しい副作用がされるか
memo.icon
副作用ってのは、APIサーバー(プロセス)とは別のプロセス/システムに対する操作のこと。正しくいくのかって話。
ここではモックを使ってもいいし/使わなくてもいい。まあ最後はモックを使わずに本物でテストをするのが安全でしかないけど。
3. セキュリティ系
権限のないリクエストに対して正しいレスポンスを返すか
4. 内部の突然の異常に対して適切な挙動をするか
トランザクションは効いてるか
ユーザーには適切なレスポンスを返してるか
その他不整合に繋がる状況は起きないようになってるか
その他なんかありそう。
良い単体テストには条件がある
1. 退行(リグレッション)に対する保護
つまり回帰テストとしてちゃんと使える状態になってるってこと。
回帰テストはまじで品質にとって大事。特にシステムを変更する際に効いてくる。
これがあるとシステム変更の際のバグ検出率が一気に上がる。むしろこれないと不安まである。
2. リファクタリングへの耐性があるか
SUT(テスト対象)の仕様が変わってないが、内部が変わっただけで失敗するテストはだめ。
テストでは、SUTの内部を触らないこと。
常に仕様(出力/結果/副作用)に対してテストを書く。
そうすれば仕様変更しない限りテストが壊れない。すぐ壊れるテストは脆いテストと言う。
なお、副作用も無くせるならできる限り無くしたいね。。。
副作用はバグを生み出しやすいから。
-> 副作用を避ける
3. テスト時間は短く
単体テストはすぐに結果がわかるようにしたい。
ビルドの度に動かしたいレベル。
すぐ結果を得ることで開発の品質をすぐに把握し修正スピードを上げれる。
4. 保守がしやすい
テストコードの意味がわかる。
どう言うテストしてるのかわかる。
理解しやすい。変更しやすい。
テストしやすい。
思考
hr.icon
データベースは共有依存だからモックすべき?
onigiri.w2.iconの考え
主張としては「基本的にはモックしなくて良いんじゃない?」と思ってる。
共有依存は、テストケース間でその状態に依存するってこと。
つまり、あるテストケース実施が、その状態を通じて他テストケースに影響し得る可能性があることを指してる。
たしかに、DBは普通に使ったら共有依存に当たるだろう。
けど、テストケースごとにDBの内容を初期化しておけば、別にテストケース間で状態を共有することにはならないと考える。
毎回、真っ新な状態にしておき、各テストケースが始まる際に必要な状態を作っておく。
これだと共有依存にはならないはず。なのでDB用にモックを作る必要もないかなと。
反対意見や懸念
テスト速度の問題
各テストケースごとにDBの初期化やクリーンナップをしてたんじゃ、時間がかかりすぎる可能性あり。
onigiri.w2.icon
確かにシステムが大きくなっていき、テストケースの数が増えれば増えるほど問題が顕著になりそう
ただ小規模なうち、問題が顕著化する/しそうになるまでは良いんじゃない?とか思ったりする
初期化/クリーンアップ処理が複雑じゃない?問題
onigiri.w2.icon
これは現代のORマッパーとか使ってたら行けるんじゃない?とか思ったりしてるけど...
正味、問題になりそうではある。
リソース逼迫するんじゃない?問題
onigiri.w2.icon
これも小規模なうちは顕在化しない問題。
大きくなってくるとやばそう。統合テストでしかやっちゃダメになる可能性あり。
結局、テストはどういう分類方法が良いんだろうなぁ
Googleのテストサイズ的な考え方か。
Small: 単一のプロセスで完了するテスト
Medium: 単一のマシンで完了するテスト
Large: 制限なし。外部システムとのやりとりまで含めて全部
統合テスト、単体テストって考え方か。(これは曖昧すぎるけど)
単体テスト: 古典派定義の単体テスト
統合テスト(内部): サブシステムのみのテスト。外部システムは全てモック化
統合テスト(外部):外部システムも全て本物で対象サブシステムをテスト
全体テスト:E2Eでやる感じかな。End to End。
onigiri.w2.icon
まあ、なんにせよ、分類の定義と役割を決めてあげれば良い話
一旦は以下でいいんじゃない?
Small: 単一プロセスで完了するやつ(単体テストと思えばいい。けど古典派で)
モック化するのは原則、共有依存と外部プロセス依存のみ
Medium:単一マシンで完了するやつ(統合テスト的な役割にできる)
Large:制限なし(本番と完璧に同様の状況でテスト。マニュアルテストじゃね?これ)
あとで読む
hr.icon
https://medium.com/clarityai-engineering/sociable-or-solitary-unit-tests-choose-your-tradeoffs-7ced14d4baef
https://marcingryszko.medium.com/what-is-the-system-under-test-a-tale-from-gallic-wars-4856783708f6
非純粋関数の場合、引数以外にも入力があるし、戻り値以外にも出力がある