経験ゼロでもできるプログラミング現場の単体テスト

  • 465
    いいね
  • 0
    コメント

経験ゼロでもできるプログラミング現場の単体テストを読んだので、そのまとめ

はじめに

著者曰く、

初めて導入する単体テストの指南書

を目指した一冊。

単体テストを書いたことが無かったり、書いたことがあっても書き方の方針が定まっていない人にはおすすめの本。
逆に既にバリバリテストコードを書いてる人に、再確認的な内容になるかも。

単体テストについて基礎知識

  1. アプリケーション開発では設計に時間がかかりすぎて、テストに時間をかけられないことがある。
  2. 致命的な障害発生する可能性がある。
  3. とはいえすべてを想定してテストを実施することはできない。
  4. テストケースを絞る必要がある
  5. 各テストフェーズの礎となる単体テストが重要になる

高品質なシステム

ユーザーを満足させ、不安や不満をいだかせないこと

  • システムエラー:データの破損などの心配が生まれる
  • 応答が遅い:いらいら・二度と使わなくなる

まとめてテストはダメ

  • 調査時間がかかる・問題箇所が切り分けづらい
  • 現場のモチベーションが下がる
  • リスクの先送り

プロジェクト終盤で行うテストは再現・原因調査・修正・再テストと時間がかかるため、プロジェクト序盤から実施して置くべき。

テストクラスのメリット

  • 繰り返し実施が可能
  • 結果が分かりやすい
  • テスト実行された箇所が分かる
  • デグレの早期発見

テストの種類と目的

テスト 内容 詳細・具体例
単体テスト プログラム単体の振る舞い確認 引数のデータの組み合わせ計算結果が正しいか、DBに正しいデータが登録されるか、例外が発生したさいの振る舞いが正しいか
結合テスト 単体テストが完了したプログラム間のデータの受け渡しを確認 登録ボタンを押下したら、フォームの値をDBに登録し次の画面に遷移するか
システムテスト 本番稼働にデータ、状態でシステム用件を満たすか確認 外部システムとの連携、日次/週次/月次/年次などのタイミングに発生する処理、システムダウン
負荷テスト システムに負荷がかかっても問題なく稼働するか確認 大量のデータを持つDBに大量のリクエストを送信し、想定時間内にレスポンスを返すか、どれくらいのレスポンスで返せなくなるか、長時間稼働によるメモリーリーク確認
システムテスト システムの脆弱性を突かれてシステムダウンや機密情報の閲覧をされないか確認 バッファーオーバーフローなどのセキュリティホール、SQLインジェクションなどの不正アクセス

代表的なテスト手法

ホワイトボックステスト

ソースコード中の文、分岐、条件などを網羅的に実行してバグを見つけるテスト。

テスト 内容
ステートメントカバレッジ(命令網羅) ソースコードの全命令・文のうち、1回でも実行されたステートメントの割合
ブランチカバレッジ(分岐網羅) ソースコードの全分岐のうち、1回でも実行された分岐の割合
コンディションカバレッジ(条件網羅) ソースコードの分岐に設定されている1つ1つの条件が成立・不成立の両方が実行されたの割合

ブラックボックステスト

入力された値に対し、仕様通り結果が返るかテストする。
次の両方を行う。

テスト 内容
同値分割法 プログラムが期待する「有効同値」、「無効同値」を入力とし結果を確認
境界値分割法 プログラムの「有効同値」と「無効同値」の境界となる値を入力し結果を確認

バグの発見は難しい

単体テストに限らずバグを発見するには、豊富な知識や経験が必要。
入力順番を変えた場合、異なるユーザが同じ処理を行った場合など、テストだけでは分からないバグや意図しない動作をたくさん発生させることができる。
プロジェクトメンバーの経験が不足している場合には、熟練者のレビューや他のプロジェクトのバグ対応を共有させてもらう。
時間が掛かるが、単体テスト段階からこのように高いレベルでテストを実施できると修正コストも低く抑えることができる。

負荷テストのタイミング

通常結合テストが終了してから行うことが多いが、ボトルネックになる原因はだいたいパフォーマンスの出ないSQLか、メモリを湯水のごとく使う実装。
つまり、実装段階で作り込んでいる。
早い段階で負荷テストを行えば、後々その場しのぎの対応を防げる。

具体的な単体テスト方法・注意すること

テストクラスの基本系

テストクラスは、JUnitの規約に従って作成すると、開発者ごとにばらつかず、可読性が高くなる。

  1. テストの前提条件をそろえる
  2. テスト実行
  3. 実行結果からテストの成否を判定

単体テストを楽にするには

  • テスト範囲を狭くする
  • 作った側からテストを行う

JUnit4のアノテーション

アノテーション 説明
@Test テストメソッド
@Before 全てのテストメソッド実行前に行われる処理
@After 全てのテストメソッド実行後に行われる処理
@BeforeClass テストクラスのテスト実行前に1度だけ行われる処理
@AfterClass テストクラスのテスト実行後に1度だけ行われる処理

テスト結果を判定するメソッド

  • assert***
  • fail

などなど

テストケースの考え方

様々な引数のパターンを考える

  • 引数にnull
  • 配列サイズが0
  • byteの値がByte.MAX_VALUE
  • byteの値がByte.MIN_VALUE
  • byteの値が0

DBアクセスの単体テスト

DBのテストには注意することが沢山ある。

  • 見る権限の無い人に個人データを見せていないか
  • 間違ったデータを登録していないか
  • DBの状態を常に等しいか
  • O/Rマッピングフレームワークを使用している場合、SQLが正しく発行されているか

こうした作業を大幅に軽減、自動化するツールが「DbUnit」。

DbUnitを使用したテスト手順

  1. DbUnitでDBとコネクションを確立(@Before/@After)
  2. テスト前のDBの状態をバックアップ(@Before)
  3. テストの前提となるデータの設定(@Before)
  4. テスト実施(@Test)
  5. 2.でバックアップしたデータをDBに戻す(@After)

テストデータの注意点

  • SELECTのテスト
    • 1件だけ取得
      • 全てのテストデータを別にする:selectされたデータを明確にするため
      • 条件を満たさない場合取得できないことを確認する
    • 複数件取得(検索条件に主キーを指定しない場合も含む)
      • データは3件以上(検索条件に合致2件以上、合致しない1件以上):複数件取得、データマッピングが正しいか確認するため
      • 「<」「>=」を使用している場合、同値ケースのテストをする
      • AND、ORをつなげてる場合、その組み合わせも網羅的にテストする
  • INSERTのテスト
    • null以外のデータを登録
    • nullを許可するカラムにnullを登録
    • カラム全てに最大桁のデータを登録
    • 一意制約に違反するデータを登録
  • UPDATEのテスト
    • null以外のデータを登録
    • nullを許可するカラムにnullを登録
    • カラム全てに最大桁のデータを登録
    • 更新対象のレコードがないときの更新
    • 複数券のレコード更新
  • DELETEのテスト
    • 存在しないレコードの削除

DBアクセスのテストケースを減らす方法

WebアプリケーションではSQLのテスト不足によるバグがボトルネックになることが少なくない。
何十行もあるSELECT文のテストや親レコード有無を考慮したテストデータ作成は面倒。
テーブル設計段階からSQL文のテストしやすさを意識する。

テストしやすいテーブル設計

  • アイデンティファイアの追加:IDカラムを追加することでテーブル結合に必要なカラムを減らす
  • DB制約を使う:制約上DBに存在しないデータのテストが必要なくなる

保守面からテスト容易性を高める

  • SQL文とメソッドの粒度を1対1に:対象が分かりやすい
  • 大作SQLはほどほどに:テストケースが作りづらい
  • SQL文での計算は控える:バグの原因がプログラムかSQL文か切り分けやすくなる

外部リソース、システムのテスト

Mockオブジェクト使用時の注意点

  • Mockオブジェクトを作成するには設計が必要:振る舞いを真似るため仕様を理解する必要がある
  • Mockオブジェクトを作成してテストしてもバグは出る
  • 対象クラスの仕様変更があるとMockオブジェクトの修正が必要になる

外部リソースにアクセスするテストの注意点

  • ファイル入出力などの例外が発生する処理と、ロジックを分割する:余計な例外を考慮する必要がなく処理が単純になる
  • メール送信などの外部システム連携が必要な場合には、連携部分と、ロジックを分割する

外部システム連携と画面側のテスト

単体テストフェーズでは、外部システムと連携できな場合も多いので、Mockオブジェクト、フレームワークを利用する。

画面側の単体テスト

  • EMMAでカバレッジを測定する
  • キャプチャリプレイツールを使用する

よいテストの条件

  • 目的が明確
  • 繰り返し実行が可能
  • 環境に依存しない(ローカル/サーバ、OS、実行環境、コンパイル環境を意識する)
  • テストが独立している
  • 処理の依存関係が疎である

継続的にテストするために

テストは行程で作られる

設計段階からテストを考慮し、プロジェクトメンバー内で共通認識とすることでクラスの粒度をあわせ、テストしやすいソースコードを作成する。
品質を意識するのはプロジェクト内の誰かではなく、全員でなければならない。

テストを意識した開発のポイント

回帰テストを自動化するために

  • SQL文の中で使用する日付はすべて引数で受け取る
  • システム日付は全てユーティリティクラスより取得する:日付の形式や実行環境依存を防げる

発見しづらいバグを作らない

  • 単体テストでは確認困難なスレッドセーフを控える(シングルトンオブジェクトには結果を持たせないなど)
  • 文字エンコードに注意(エンコードを指定するなど):OSに依存するため

静的解析ツールを使用

  • コーディング規約の作成
  • CheckStyleによる規約チェック
  • FindBugsによる危険なコーディングの排除
  • フォーマッターの設定

車輪の再開発を避ける

単体テストは仕様とことなる実装やバグを見つけ出すために行いますが、そもそも外部で公開されているライブラリを導入することで、テストの必要性すら排除できる。

開発環境は早めに準備

早期に準備することで、コーディング規約やCheckStyle、FindBugsのルールが決定できる。
プロジェクトが進んでからだと、開発環境、コードの修正など余計な手戻りが発生する。

ビルド&テスト自動化する環境の構築

継続的インテグレーションの実現

開発プロセスの後になってからバグを見つけて修正するのではなく、継続的にビルド、テストを実行し、問題を早期に発見する。

  • バージョン管理システムからソースコードを取得
  • ビルド
  • 単体テスト
  • jarやwarにパッケージ
  • 結合環境にデプロイ これらをCIツールを使用し自動的に行う。

最後に

ここではだいぶ割愛しているが、著書の中ではJUnit、DbUnit、Mock、Apache Commonsの記述方法や各メソッド、アノテーション、Hudson(現Jenkins)の使い方
など細かく説明しているので、使用経験が無ければ一読の価値あると思います。

他にもテスト関係の2冊(現場で使えるソフトウェアテスト, JUnit実践入門)を読みましたが、一番基礎的な内容が書かれています。