#はじめに
JUnit実践入門を読んだのでそのまとめ
#Part1 JUnit入門
##なぜ、ユニットテストを行うのか?
どんなプログラマであっても人間であるかぎり間違いをおかす→正しく動くか不安→不安を安心に変える ユニットテスト
##JUnitとは?
Javaのテスティングフレームワーク。
- テストの実行フレームワーク
- テストの期待値と実測値の検証API
- テストケースのフォーマット
##日本語のメソッド名を使うメリット
英語で格好よくテスト内容を記述でき、それを英語表記でき、読み取れれば英語の方が望ましい。
日本語でのソフト開発なら、Javadocで出力すれば、テスト項目一覧、テストクラスのアウトライン参照でテストが把握できたり、テスト失敗時の内容が理解しやすいなどメリットが大きくなる。
##ソフトウェアテストの特徴
- テストにある条件下という制約があること。これは「前提条件」、「事前条件」などの使用するデータ、環境、事前操作が含まれる。これを明確にする必要がある。
- ソフトウェアの振る舞いを記録すること。記録できなければ、検証ができない。出力されるデータやDBがどのような状態か確認。場合によっては観測者が変化を知ることができれば十分な場合あり。
- 期待する結果との検証を行うこと。ランダム性の高い振る舞いは検証が難しい。
ソフトウェア開発では作成が同時にテストを行ったり、実測したテストから得られたフィードバックからテストを追加する方法が有効。
##テスト技法
- ホワイトボックステストとブラックボックステスト
ホワイトボックステスト は内部ロジックや仕様を考慮してテストケースを設計。テストが可能な限りすべてのロジックを実行するため、内部仕様を理解しているプログラマが実施。
ブラックボックステスト は外部仕様からテストケースを設計。ユーザ受け入れテストや機能テストなどの大きな粒度で検証するテストで採用。
ユニットテストはホワイトボックステストに分類されるが、内部ロジックに依存しすぎたユニットテストは変更にもろくなるため、テスト対象メソッドの外部仕様からテストデータを選択したほうがよいという側面もある。
-
同値クラスに対するテスト
同様の結果をもたらす値を同値クラスとしてグループ化し、テストデータを選択するテスト技法。通常対象グループの中から代表値を選択しテストを行う。 -
境界値に対するテスト
異なる結果をもたらす値(境界値)に直目し、その近傍からテストデータを選択する。
ユニットテストのパターン
- 自動化するためいつでも繰り返し実行できること→安心してリファクタリングができる
- 成功状態を維持するため結果が一定でない不安定なテストはさけること
- テストに定義されている動きは保証されるため、最も正確なドキュメント(仕様書)でありサンプルコードとなるので読みやすいテストを心がけること
- テスト失敗時に問題を特定しやすいよう、十分に小さな単位で可能な限り作る→失敗した時に影響範囲と条件が絞込みやすい
- 前提条件、実行、検証が複雑な可読性の低いテストコードは避ける→メンテナンス性も悪くテストの質に大きく影響を与える
- 実行順序に依存しないこと→他のテスト結果に影響を受けるテストは予期しない形で失敗することになるため
可読性の高いテストコードの書き方
- 「テストを行う前提条件」「テストに用いる入力値や操作」「テストを行った時に期待する値や動作」が重要ポイント
JUnitのアノテーション
#Part2 JUnitの機能と拡張
JUnit4のアサーション
JUnit4ではassertThat(Matcher APIと併用)とfailでほとんどの判定を行えるあためこの2で行う
Matcher API
テストランナー
JUnitCoreクラス
EclipseなどのIDEを使用しているビルドツールを経由してJunitCoreが呼ばれるため意識することがないが、実際には以下の流れで呼び出される。
- テストケースの収集
- テストの実行
- テスト結果の出力(レポート)
テストランナー
テストランナー | 機能 |
---|---|
JUnit4 | JUnit標準のテストランナー。実行対象は、「publicメソッド」、「Testアノテーション」、「戻り値がvoidで引数がないこと」が条件になる。 |
Suite | Suiteクラスは複数のテストクラスを束ねてテストを実行するテストランナー。関連として「~Tests」と複数形にし、「AllTests」はよく使われるテストスイートクラス名。 |
Enclosed | 構造化したテストクラスを実行する。同じ初期化などをまとめて構造化するときに効果的。 |
Theories | パラメータ化したテストケースを実行する。テストケースとテストデータを分割することで同一のテストケースを別パラメータで複数回の実行が可能になる。 |
Categories | 特定のテストクラスをまとめて実行する。テストケースが増えるとテスト時間が長くなるので、テストケースをカテゴライズして必要なケースに分けて実行する。 |
JUnitCoreはテストクラス単位なので、全クラスや特定のクラスのテストを行うためにはテストランナーを利用する。
テストランナーはテストクラスごとにorg.junit.runner.RunWithアノテーションを利用して設定する。
省略した場合には、JUnit4が利用される。
テストのコンテキスト
テストコードはプロダクションコードと同様に可読性が必要だが、テストコードをプロダクションコードと同じ方法で整理すると逆に可読性が下がる場合があるので、共通したコンテキストを持つテストケースをEnclosedなどを利用しグループ化する。
コンテキストパターン
パターン | 解説 |
---|---|
共通データに着目する | 入力値や期待値などのテストで使用するパターンに着目し、共通するテストをグループ化する。 |
共通の状態に着目する | テスト対象クラスが状態をもつ場合、事前処理として行いテスト対象処理と分離する。 |
コンストラクタのテストを分ける | インスタンス化のテストは生成自体がテスト内容のため、他のテストケースと分離する。 |
テストクラスを横断する共通処理
共通する処理を記述する、基底クラスを定義し、継承するのは極力さける。
理由は、テストクラスに不必要な初期処理を動かしたり、基底クラスの変更が継承したクラスすべてに影響を与えるため。
共通処理はユーティリティクラスに抽出し利用すべき。
##テストフィクスチャ
テストで扱うデータやテスト実行環境、オブジェクトの状態のこと。
ユニットテストのフィクスチャは以下。
- テスト対象オブジェクト
- テスト実行に必要なオブジェクト(入力値)
- テストの検証に必要なオブジェクト(期待値)
- テストの実行までに必要なテストオブジェクト
- ファイルなどの外部リソース
- データベースやソケットサーバなどの外部システム
- 依存クラスや依存外部システムのモックオブジェクト
フィクスチャはテストケースごとに独立し、テスト実行ごとに初期化、終了時に開放するのが基本的戦略。
この戦略を フレッシュフィクスチャ と呼ぶ。
フィクスチャが共有されると実行順序によって影響を受ける。またユニットテストを並列実行した場合、共有フィクスチャが複数テストから利用されるため問題は深刻。
JUnitではテストケースごとにインスタンス化を行うため、フィクスチャは共有されないが、シングルトンやDBなどの外部リソースは共有されるため注意が必要。
##フィクスチャのセットアップの種類
セットアップ | 説明 | メリット | デメリット |
---|---|---|---|
インラインセットアップ | テストメソッド内でフィクスチャのセットアップを行う | メソッド内でテストコードが完結し見通しがいい | セットアップ量が多いとメソッドが膨大になり、テスト対象が曖昧になる |
暗黙的セットアップ | セットアップメソッド内でフィクスチャのセットアップを行う | テストメソッド内が実行と検証が中心になり、テスト内容が明確になる | テスト対象メソッドを構造化しない限りすべてのテストメソッドで共通のセットアップしか行えない |
生成メソッドでセットアップ | フィクスチャの生成処理を行うメソッドを定義する。クラス間で共通処理に関しては生成用クラスを作成しstaticメソッドとして定義する。(staticインポートができるため)メソッドは拡張性ではなく、可読性を重視する。 | メソッド単位で共通処理を抽出できる。 | 複雑なオブジェクト生成はなくならず、読みやすくはない。 |
外部リソースからのセットアップ | YAML,XML,JSON,CSV,Excelなどの外部ファイルにテストデータを記述し、読み込む。 | テストコードがシンプルになる | テストコードとテストデータの相互参照が行いにくい。 |
Javaによる宣言的なセットアップ | 匿名インナークラスを用いてオブジェクトの初期化を行う | Javaのコードのみで行えるのでIDEの恩恵を厚く受けられる | private,finalフィールドにデータのセットアップができない |
Groovyによる宣言的セットアップ | Groovyを用いてJavaの宣言的セットアップを行う | Javaではできなかったprivate,finalなフィールセットアップもできる | Groovy環境の導入が必要になる |
##パラメータ化テスト
テストでは大抵1つの入力値で不十分で複数の入力値と期待値のパターンが必要になる。
ただし、1メソッドに複数の期待値の検証を行ったり、同じ入力値と期待値は別でもテストコードが同一になるテストケースは冗長となる。
そこで適切なデータパターンを抽出し、テストランナーTheoriesを利用してパラメータ化のテストを行う。
Theoriesで使用できるアノテーション
アノテーション | 説明 |
---|---|
@Theory | テストメソッドに指定する。対象メソッドはパラメータを引数で受け取ることができ、複数の引数を受け取った場合にはすべての組み合わせが実行される。 |
@DataPoint | パラメータを定義するアノテーション。パタメータはpublc staticなフィールド、またはメソッドで定義する。 |
@DataPoints | 複数のパラメータを定義する。@DataPoint同様の定義だが、配列で定義するため、複数パターンのパラメータ定義ができる。 |
ルール
JUnitではテスト間で共通する処理はサブクラスに抽出するしかなく、テストクラスはテストのメタ情報にアクセスできなかった。
そこでJUnit4.7からルールが追加された。
また、カスタムルールの導入により
チートシート
カテゴリ化テスト
テストが増えてくるとテストに時間のかかる スローテスト問題 発生する。この対策として主に以下の4方法がある
- テストの実行時間を短くする
- テストの実行環境を強化する
- テストを並列で実行する
- 実行するテストを絞り込む
カテゴリ化は上記の4が対象になる。
#Part3 ユニットテストの活用と実践
##テストダブル
テスト対象クラスが他のクラスに依存していない可能性は少なく、場合によっては外部システムや乱数、システム時間などに依存する場合もあります。
そこで依存するオブジェクトの「代役」としてモックやスタブを利用する。
モックやスタブの作成は複雑な処理が必要な場合もあるので、公開されてるライブラリを利用する。
チートシート
##データベースのテスト
Webアプリケーションなどのソフトウェア開発ではDBを利用しない場合はほとんどない。
DB周りのテストを行う場合、インターフェイスが決まっていればモック化も可能だが、DBを使っていないと発生しない問題を見逃す可能性が発生する。
そこでプロダクション環境に近い環境を構築するとるとテスト環境ごとにセットアップしないといけなくなる。
そこで軽量データベースのH2 DatabaaseやSQLiteを利用する。
ただし、DB固有の方言があるため、HibernateDomaといったORMを使用するとDBごとの差異を吸収できる。
いずれにしてもプロダクション環境と異なると環境で予想外の問題が発生する可能性はリスクとして考慮すべき。
コードカバレッジ
実行したユニットテストの品質の基準としてコードカバレッジがある。
カバレッジを測定しその傾向を分析することテストケースの問題を検知することができる。
名前 | 説明 |
---|---|
C0(命令網羅) | プログラム中の全ての命令が1回以上実行を測定 |
C1(分岐網羅) | プログラム中の各分岐について1回以上実行を測定 |
C2(条件網羅) | プログラム中の判定の組み合わせを測定 |
EclEMMAによるガバレッジ測定
Eclipseのプラグインとして簡単に導入できる。
ガバレッジの測定をグラフィカルに確認でき、既存のプロジェクトの修正を加えず利用できる。
#Part4 開発プロセスの改善
##継続的テスト
ソフトウェア開発では複数のテストを行い欠陥がある場合には修正を行う。
しかし、欠陥を修正した欠陥(リグレッション)が発生することがる。
このようなリグレッションを早期に発見するため継続的なテストを行う。最近の開発では継続的インテグレーションツールを利用することで以下のような恩恵を受けられる。
- 開発チームへの素早いフィードバック
- 早期からテストする
- 自動的にテストする
- 繰り返しテストする
また継続的なテストを実現する 「現代ソフトウェア開発の三本柱」 として以下がある。
- ユニットテスト
- 自動化(自動ビルド)
- バージョン管理
ビルドの自動化
Ant,Maven,Gradleなどの自動化ツールを利用することでビルドプロセスの自動化が行える。
Mavenでは多くのプロセスが標準、あるいはプラグインにより提供される。以下は代表的なもの。
- プロジェクトの構成
- 依存ライブラリの管理
- ソースエンコーディングの設定
- ビルドの実行
- カテゴリ化のテスト
- カバレッジレポート
また継続的なテストツールとして代表的なものとしてJenkinsが用いられる。
Jenkinsを用いることで理解しやすいUIで「現代ソフトウェア開発の三本柱」を実現することができる。
テスト駆動開発
テストコードをプロダクションコードより先に書くことを中心としたソフトウェア開発技術。
プロダクションコードをテスタビリティの高いシンプルな設計に導くことができる。
主な目的は以下。
目的 | 説明 |
---|---|
ステップバイステップ | 1回の小さなサイクルを繰り返すことでミスを大幅に減らし、問題をしい作することで難易度が下がり、思考停止する時間もへらすことができる。 |
自分が最初のユーザ | クラスやメソッドの使いづらさに気づき修正することで、ユーザビリティの高いコードができる。 |
動作するきれいなコード | 完璧な設計はできず、できてもコストがかかる。テスト駆動開発はテスト成功後リファクタリングを行うことできれいなコードができる。 |
すばやいフィードバック | 継続的インテグレーションツールを導入していればすばやいフィードバックを得られ、プログラマは安心して開発できる |
メンテナンスされたドキュメント | テストコードは確実に動作することが保証されるため、テストコードがドキュメントであり、サンプルコードになる。 |
サイクルついては以下。
- 設計する
- テストコードを書く(レッド:テスト失敗)
- プロダクションコードを書く(グリーン:テスト成功)
- リファクタリングする(グリーン:テスト成功)
これを繰り返す。
振舞駆動開発
ユニットテストでは粒度が細かすぎ、顧客要求を満たしているか保証できない。
そこでもっと大きな単位(システムテストや受け入れテスト)を実現するため、シナリオ単位で行う振舞駆動開発が用いられる。
Javaにおいては cucumber-juit を使用することで振舞駆動開発が実現できる。
##おわりに
他に読んだテスト関連の2冊(現場で使えるソフトウェアテスト,経験ゼロでもできるプログラミング現場の単体テスト)の中でタイトル通り特にテストコードの記述に関して深く言及されている。
後半はだいぶ省略してるが、JUnit4で利用できる具体的で実践的な記述も豊富にあるので、テストコードを書く人は是非読んでもらいたい一冊。