ソフトウェアテストを学ぶ
「知識ゼロから学ぶソフトウェアテスト」
成果物の品質を上げたいと思い、テストについてまとめられた書籍を購入した
書籍を読んで、要約、自分が感じたこと等をまとめる
テスト担当者の心得
バグを全部見つけるのは無理だと心得ろ
- ソフトウェアには無数のバグが潜在し、ゼロにするのは難しい
- 優先順位を設け、品質に致命的なバグを潰す
エラーは見つからないだろうという設定のもとにテストの計画を立ててはいけない
- 車の運転と同じで、エラーはないだろうと思うとバグを見逃す
- 常にバグがあるかもしれないという姿勢でテストに臨むべし
プログラムのある部分でエラーがまだ存在している確率は、すでにその部分で見つかったエラーの数に比例する
- バグはプログラムの特定の部分に偏在している(バグの47%はプログラムの4%の部分に偏在しているという例)
- 複雑なプログラムの箇所でバグが見つかったらなら、まだ別のバグが残っているかもしれないと考えた方が良い
ソフトウェアテストで重要なのは、どの部分にバグが出やすいのか、そこにどのような手法を適用すれば十分な品質が得られるかを知ることである
完全無欠なソフトウェアテストは可能か?
- まったくバグのないソフトウェアは存在しない
- 「完璧」ではなくとも「十分」な品質を持つソフトウェアは効果的なテストで実現できる
品質向上のための投資は、投資額が修正にかかる費用を超過するか、「もっとましなことをすべきだ」と誰かが言い出すまで増加しつづける
テストケースを書くことはプログラムを書くよりも難しい
ホワイトボックステスト
- 「どのテスト手法を適用すべきなのか」を常によく考える
- 限りある時間の中で効率的にテストを実行する
ホワイトボックステストは論理構造の正しさのみをテストするため、ソフトウェアの仕様が間違っていることから起こるバグは発見できない
制御パステスト法
- プログラムがどのような振る舞いをして、どのように制御され実行されていくかをテストする
- 制御パステストによるコードカバレッジテストの本質は、様々に分岐するフローチャートをきちんとカバーすることにある
ステートメントカバレッジ
ステートメントカバレッジは、コード内の命令文(ステートメント)を少なくとも一回は実行する
ステートメントカバレッジでは、a > 1
がtrue
となるケースを1回実行することでテストを終了するため、a == 0
などのパスが抜ける非常に弱いテスト手法
また、そもそもif a > 1
の部分をif a > 0
と書いてしまった場合のバグは発見できない
graph TB
A[Start] --> B{if a > 1};
B -->|Yes| C[A];
B -->|No| D[B];
ブランチカバレッジ
ブランチカバレッジは、分岐コードに対してtrue
とfalse
の結果を少なくとも一回ずつ持つようにテストケースを実行する
- テストケースの数が増大する
- ある特定のバグが発生しやすい部分にブランチカバレッジを実行するのが効果的
カバレッジ基準
カバレッジ基準とは、テストがどれだけ網羅されているかを示す基準値
- 一般の商用ソフトウェアなら60~90%程度で十分
- カバレッジ測定せずに出荷するのはリスクの高い行為
- カバレッジの値が高いから絶対に大丈夫とは言えない
Visual Studioにもコードカバレッジ分析の機能はあるがVisual Studio Enterprise
に限定されている
nugetで提供されているC#用の分析ツール
- Coverlet カバレッジ測定ツール
- ReportGenerator レポート出力ツール
カバレッジテストで検出できないバグ
- プログラムのループに関するバグ
- 要求仕様自体が間違っていたり、機能が備わっていないバグ
- データに起因するバグ
- タイミングに起因するバグ
プログラムのループに関するテストケース
- ループをしない、ループ自体がスキップされるケース
- ループ回数が1回のケース
- 典型的なループ回数のケース
- 最大ループ回数より1少ない数でのケース
- 最大ループ回数でのケース
- 最大ループ回数 + 1でのケース
要求仕様自体が間違っていたり、機能が備わっていないバグ
- 仕様自体が間違っていても、テストでバグを検出できない
- 機能自体がごっそり抜け落ちていても検出できないので、要求仕様書をテスト仕様書と付き合わせてチェックする必要がある
データに起因するバグ
mutex
等、複数のプログラム(タスク)が同時に処理を行う環境(並列処理)で、共有資源に対して同時にアクセスしても問題がないようにする- データベースの中身が変化することでバグが出たり、出なかったりするケース
タイミング(マルチタスクや割り込み)に起因するバグ
- データがプロセス・スレッド間で共有されているかをチェックする
- 共有されているデータへのアクセスパターンをテストケースに落とし込む
- プロセス・スレッドの生成と削減の組み合わせをテストする
- できるだけたくさんのプロセスとスレッドを立ち上げてテストする
テスト駆動開発
Test Driven Development (TDD)
- まず失敗するテストを書いて、その後にテストが通るようにコードを実装する
- 開発スピードが向上し、変更に耐性を持たせることができる
- リファクタリングや仕様変更の際に、修正の影響範囲がわかりやすい
テスト駆動開発でバイブルとされている2冊 * テスト駆動開発 Kent Beck (著), 和田 卓人 (翻訳) * 実践テスト駆動開発 Steve Freeman (著), Nat Pryce (著), 和智 右桂 (翻訳), 高木 正弘 (翻訳)
TDD Boot Camp 2020 Online 基調講演のアーカイブがYouTubeで公開されていて参考になった
ブラックボックステスト
ブラックボックスはプログラムを一種のブラックボックスと見立て、ソースコードを見ずにテストを行う手法
「ソフトウェアは4つの仕事しかしない。その4つの振る舞いをテストすれば良い」
- 入力を処理する
- 出力を処理する
- 計算を行う
- データを保存する
入力と出力のテストする「同値分割法」と「境界値分析法」
- まず、境界値テストで、境界値に関わるバグを潰す
- 入力値が2つ以上あればディシジョンテーブルテストをする
- 遷移があれば状態遷移テストをする
同値分割法
- 入力領域を「同値クラス」という部分集合に分割
- 部分集合に入る入力値を等価とみなす
上記の例の場合は、入力値を2つの部分集合(無効同値と有効同値)に分けることができる
テストケースには、この2つの部分集合を網羅するように用意する境界値分析法
- プログラムで「境界」と呼ばれる場所には常にバグが潜んでいる
- 境界値近くは詳しくテストをする必要がある無効同値
境界値のバグパターン
// [正常]
if (0 < a) { }
// [異常] 0 <= a としてしまう
if (0 <= a) { }
// [異常] 1 < a としてしまう
if (1 < a) { }
// [異常] 余分な境界を加えてしまう
if (1 < a && a < 10) { }
入力ダイアログボックスには境界値テストが効果的
On-Offポイント法
- 境界値をテストする一般的な方法
- 異なる処理が行われる一番近い点をテストケースとする
上記の例だと、「0」と「1」が異なる処理の境界となる
ディシジョンテーブル
- すべての入力の組み合わせを表にして、入力に対する動作もしくは出力を明記する
- 表に状態やルール、動作などをまとめる
- テストケースの抜けを発見しやすい
- 項目数が少なく、複雑な処理をするプログラムに効果的
1 | 2 | 3 | 4 | ||
---|---|---|---|---|---|
10未満 | Y | N | N | Y | |
条件 | 10~30 | N | Y | N | N |
30以上 | N | N | Y | N | |
動作 | 正常出力 | x | |||
エラー処理 | x | x | x |
複数の入力ダイアログボックスにはデシジョンテーブルテスト
GUIのテスト
状態遷移テスト
* 状態state
と遷移transition
の2つで表現される
* 入力X
によって状態がA->B
と遷移するテスト
状態遷移テストで見つかるバグ * 期待していない状態に遷移するバグ * 遷移自体ができないバグ
状態の数が増えすぎるとテストが難しくなる
状態遷移テストに適したソフトウェア * GUI(Graphical User Interface)ソフトウェア * オブジェクト指向ソフトウェア * 通信プロトコルテスト
ダイアログボックスの遷移があれば、状態遷移テストを行う
ランダムテスト
やみくもに行うテストで、様々な呼び方がある * アドホック(ad-hoc)テスト * アドリブテスト * ランダムテスト * モンキーテスト
探索的テスト
- 探索的テストとは、ソフトウェアの理解とテスト設計、テストの実行を同時に行う
- 「この振る舞いがおかしくないか?」「処理が遅いのではないか?」等、どうあるべきかを考えながらテストを行う
- 正常値と異常値を両方入れてみて、どうなるかを確かめてみたりとテストを行う担当者のスキルに依存する
手順
- クライテリア(どういうソフトウェアであるべきかの判定基準)を決める
- 探索的タスクを実行する
探索的テスト
- ターゲットソフトウェアを決定
- 機能をリストアップ
- 弱いエリア(バグが出そうな場所)を見つける
- 各機能のテスト及びバグの記録(テストケースは細かく書かず、テスト実行に時間を割く)
-
網羅的なテストの実行(バグ修正の影響の確認、環境や担当者を変更しての再テスト)
-
非機能要求への探索的テストは、ユーザビリティテスト以外は良い結果が出ない
- ユーザー視点でテストするのが探索的テストである
- 探索的テストは担当者が対象のソフトウェアのドメイン知識が豊富な場合に抜群に力を発揮する
非機能要求テスト
パフォーマンステスト
ソフトウェアの性能が期待通り出ているかをチェックするテスト
- 設計時にソフトウェアのパフォーマンスを明確な数値で定義(時間・データサイズ等)
- バグを発見するために、テストケースは要求定義を逸脱するように設計する
- パフォーマンステストは後回しにせず、定期的に行う
パフォーマンステストの手順
- アーキテクチャバリデーション
- パフォーマンスベンチマーク
- パフォーマンス回帰テスト
- パフォーマンスチューニング、アクセプタンステスト
- 24x7パフォーマンスモニター
セキュリティテスト
悪意のある攻撃に耐えうるかを確かめるテスト手法は存在しない
セキュリティ攻撃の種類
- プログラム構造の不備を突き、プログラムの制御を奪ったり、不能にする
- プログラムのデータの不備を突き、そのデータを改ざんする
上記のセキュリティバグを見つけるようにテストする
セキュリティリスクの事例
- 認証不十分な入力
- 不完全な認証・セッション管理
- インジェクション
- アクセスコントロールミス
- クロスサイトスクリプティング
- セキュアでないオブジェクトへの直接参照
- バッファーオーバーフロー
- クロスサイトリクエストフォージェリ
- インジェクションフロー
- セキュリティ設定の間違い
- 設定情報の漏洩、不適切なエラーハンドリング
- セキュアでない暗号ストレージ
- URLアクセス制限の不備
- DOS攻撃
- 不十分なトランスポートレイヤーの保護
セキュリティに関する脆弱性情報の収集方法として、例えばIPAが公開しているicat for JSON
などがある
モジュール指向のテスト
- アプリケーションを基本モジュールに分解する
- モジュールに対する入力を定義する
- その入力によってモジュールの脆弱性が露見するかどうかを判断する
- 脆弱性があると考えられるモジュールに様々なデータを入力する
基本的なセキュリティテスト手法
- 解析 (Analze)
- 変更 (Rewrite) or 削除 (Remove)
- 監視 (Monitor & Audit)
ソフトウェアテストの基本
テストプランの書き方
IEEE829
のテストプランテンプレートを参照する
テストケースの書き方
- いつ、誰がやっても同じような結果が得られるように書かなければならない
- テスト管理ツール
テストケース生成ツールの使用も検討する * PICT (ペアワイズ法による組み合わせテストケース生成ツール)
複雑なコードほどバグが出やすい
- 複雑度が高ければ高いほどリスクが高い
- 小さく、シンプルなコードはバグが出にくい
テストの自動化
テストの自動化に失敗する理由 * テストコードのメンテナンスコストがかさむ * 自動化に期待しすぎる * トレーニング不足・テストコードのレベルの低さ * 誤ったツール選択
テストの自動化スクリプトをどのように書くか、メンテナンスのしやすいテストコードの書き方を正しく学ぶことが必要
自動化に向くテスト * スモークテスト(開発途上のソフトウェアをテストする手法) + パフォーマンステスト(たくさんシミュレートしたい) * APIのテスト
自動化に向かないテスト * 回帰テスト(メンテが大変) * グラフィックやサウンドなどのメディア関連のテスト(自動化できない)
グローバル変数を使用すると、組み合わせテストのテストケースが増大してしまう
アーキテクチャに問題があると、テストコストが増大する