スポンサーリンク
概要
以前JUnit実践入門を紹介しましたが、同じくJUnitをテーマとした技術書として、オライリーから発売されている「実践JUnit」を読んだのでその感想を書こうと思います。
技術書のセールとおすすめ書籍を紹介しています。合わせてご覧ください。
スポンサーリンク
書籍概要
どんな本?
Javaユニットテストのデファクトスタンダード「JUnit」の解説書。モダンなJava開発においてユニットテストはいかなるアプリケーションを開発する場合にも欠かすことのできないプロセスです。本書では、ユニットテストの基礎からチーム開発でのユニットテストまで、達人プログラマーの実践的テクニックを明らかにします。さまざまなベストプラクティスからわかりやすい合い言葉が作られており、読者はユニットテストでの指針をすぐにマスターできるでしょう。避難は「おかし」、味付けは「さしすせそ」、ユニットテストは「FIRST」!
- Jeff Langr, Andy Hunt, Dave Thomas 著、牧野 聡 訳
- 2015年09月 発行
- 272ページ
- 定価3,080円(税込)
スポンサーリンク
スポンサーリンク
内容のまとめと感想
Javaのユニットテストフレームワークである、JUnitを使用してユニットテストコードの書き方を学ぶ事ができる本です。
ユニットテストの基本から、各種技法やあるべき考え方まで学ぶ事ができる1冊となっています。
ユニットテストを書いた事がない人が入門として読むのにも適していますし、ある程度の経験がある人がテストの考え方や綺麗なコードを書くための方法を学ぶといった点でも適していると思います。
テストコードの書き方だけではなく、テストコードがある前提でどのようにコードをリファクタリングしていけば良いか具体的な例を示しながら書かれている点も参考になりました。
この部分は以前紹介した「レガシーコード改善ガイド」に通じる部分が多くありますね。
良かった点
Java以外でも活用できる
JUnitの本ですが、JUnitに対する細かい説明は最小になっていて、様々なバックグラウンドの人がユニットテストを学ぶのに適した本に仕上がっています。
私自身もJavaやJUnitは殆ど書いた事はありませんが、非常に有用でした。
読みやすい
オライリー翻訳本にありがちな、文章の読みにくさもなく、270ページくらいで読みやすいボリュームです。
軽い気持ちで読めるのが嬉しいポイントです。
具体的なコードによる説明
実際のコードを使って、テストコードやリファクタリングの手法が書かれていてイメージがしやすくなっています。
JUnit実践入門との比較
タイトルも似た「JUnit実践入門」に関しては、本ブログでも以前紹介していますが、そちらと比較した場合に感じた点を記載します。
JUnitt実践入門は、ユニットテストだけに留まらずAndroidのテストやふるまい駆動開発、CD/CIを実現するための具体的なツールの説明など、関連する方法や仕組みなどに関しても幅広く紹介しています。
この点自体は良いとは思うのですが、こういった内容はツール自体が変化したり、コードの書き方が変化する事が多いです。実際に10年近く経った今では使わない方法やコードも含まれていました。
対して本書は、ユニットテスト自体に対する内容にフォーカスしていて、ユニットテストを書くための本質を学ぶには良く、陳腐化しにくい内容が多いと感じました。
個人的な感想としては、良いユニットテストを描きたいという目的においては本書の方が参考になると思います。
JUnit自体の細かい使い方や、関連する周辺技術なども学びたいという場合には「JUnit実践入門」も有用な本だと思います。
その他オライリー本感想
その他のオライリー本の紹介をしていますので、合わせてご覧ください。
【オライリー本感想】Seleniumデザインパターン&ベストプラクティス はUIの自動テストのガイドとして良くまとまっている良書
スポンサーリンク
読書ノート(個人的なまとめ)
第1章 初めてのJUnitテスト
UnitTestの意義から始まり、JUnitの導入方法と簡単な使い方が説明されている章です。
JUnitに限らず、xUnitの経験が既にあれば既に知っている内容が殆どなのでさらっと読んで読んで進めて良いと思います。
第2章 JUnitの実践的な利用
1章で説明したサンプルは非常に単純なコードに対してのテストコードを記述していたのに対して、本章ではより複雑な実際にあえりえそうなコードをサンプルにしてどのようなテストコードを書けば良いか説明しています。
大量にあるテストケースの数例を挙げているだけですが、こういった本当の製品コードのようなものでもテストコードは書けるということを示すために敢えてこの章を設けたようです。
第3章 さまざまなアサーション
JUnitで使用するアサーションメソッドの使用方法に関してまとめた章です。
JUnitでは旧来からあるアサーションとは別にmatchersという宣言的なアサーションを実装できるようになっており、使い方が書いてあります。
JUnitというよりあくまでxUnitのテスト技法を学びたいという人であればこの章は飛ばしても良いかも。
第4章 テストの構成
UnitTestのテストの構造や考え方といった基本的な思想と、JUnitの便利な使い方が説明されている章です。
JUnitに限らず、他のxUnitでも使用可能な考え方がまとまっているので、かなり有用な章です。
privateなメソッドに対するテストはNGというのは他の本でも言われていますが、本書でも書かれていますね。
- AAAに基づいた一貫性の実現
- AAAの3つのブロックに分けてテストコードを実装する
- Arrange:テストを実行するための基本的なセットアップを行う
- Act;実際のテスト対象のメソッドなどを実行する
- Assert:期待値のチェックを行う
- ふるまいのテストとメソッドのテスト
- 個々のメソッドをテストするのではなく、クラスとしての振る舞いをテストする
- 例:Getxxxみたいな特定のフィールド情報を返すメソッドがあったら、そのメソッド単体でテストしても意味はない。そのデータをセットアップするメソッドを呼び出してデータを作った上でテストする。
- 全体的な観点を持った上で、個々のメソッドではなくクラスとしての振る舞いをテストする必要がある
- privateなデータの公開、privateなふるまいの公開
- publicなメソッドではなく、privateなメソッドをテスト対象とすると実装の詳細に依存してしまう
- publicな振る舞いが変わっていなくても、ちょっとした修正でテストを作り直す必要が発生し、リファクタリングの意欲が奪われてしまう
- privateなふるまいをテストしないといけないというのは、設計の問題で単一責務の原則に違反しているのでクラスの分割などを考える必要がある
- ドキュメントとしてのテスト
- コード自体へのコメントの代わりに、テストコードをドキュメントとして機能させることが望ましい
- テストメソッドの名前は一貫性でわかりやすい名前をつける
- 悪い例:makeSingleWithdrawal (1回出金する)
- 良い例:withDrawalReducesBalanceByWithdrawalnAmaount (出勤を行うとその分残高が減る)
第5章 FIRST(良いテスト)
UnitTestにおける、良いテストを書くための考え方をまとめている章です。
良いテストの条件である要素の頭文字を取ってFIRSTとして、各内容を説明しています。
UnitTest周りの話をすると必ず出てくる定番的な内容ですが、それだけに一番重要な要素という事でしょう。
馴染みのないという人は、ここで書かれている原則はかなり重要なので一度押さえておいた方が良いと思います。
- 良いテストはFIRSTである
- Fast(迅速):テストは素早く終わるべき。DBやファイルなどメモリ外の処理にアクセスするテストは遅いのでスタブやモックなどが必要
- Isolate(隔離):他のテストや同一メソッド内の処理に依存しない(2つの振る舞いアサーションがあるならば別にすべき)
- Repeatable(繰り返し可能):何回実行しても常に同じ結果になること(時刻などタイミングで変わるものはモック化するなど必要)
- Self-Validate(自律的検証):CIなどにより自動でテストがセットアップされて実行される
- Timely(適切なタイミング):今書いているコードに対してテストを書く。バグが無く変更の可能性が無いコードに対して後から追加するのは効果が少ない
第6章 Right-BICEP(テスト対象)
いざUnitTestコードを書く際に、テスト対象にどのようなテストケースを考えれば良いかの指針を紹介している章です。
これらの要素の頭文字をとってBICEP(力こぶ)と名付けて説明をしています。
テストケースを考えるというのは、なかなか難しいもので、こういった指針をベースに考えると確かに見落としを減らすことができそうだと感じました。
パフォーマンスのチェックをUnitTestで行うというのは、なるほどと思いましたが、環境の問題や計測対象の絞り込み(全て作るの?)など色々と問題はありそうで、実際にやるならば色々と考えないといけないなぁと思いました。
- Right(結果の正しさ):
- テストにおいて第一なのは、コードが期待通りの結果を出しているか
- ハッピーパス(明らかに成功する実行パターン)を作成して確認を行う
- ハッピーパスがわからないならば、仕様の見直しが必要
- Boundary(境界条件):
- コードにおける境界条件(限界値や異常値)を値をチェックする(大概のバグはここでみつかる)
- CORRECTという考え方でチェックすると境界条件が設定しやすい
- 詳細は次章で説明
- Inverse (逆の関係をチェックする):
- 逆のロジックを適応してふるまいをチェックする方法
- 例1:掛け算と足し算を使用したロジック -> 結果を割り算と引き算で再計算して入力値になるかチェックする
- 例2:あるコレクションをフィルタするロジック -> フィルタの条件と逆の条件でフィルタしたものを結果にたすと入力値になる
- Cross-Check(別の方法でチェックする):
- テスト対象のロジックと同じ結果を実現する機能があった場合に、その機能と同じ結果になっているかチェックする。
- クラス内の別の情報と付き合わせて結果が矛盾していないかをチェックする。
- 例1:ある計算ロジックがあれば、Mathクラスの計算結果と比較してみる
- 例2:蔵書数を管理するロジック => 貸出数と在庫数を足したら蔵書数になるか?
- Error(エラーを強制的に発生させる):
- エラーを強制的に発生させて、ふるまいをチェックする
- どのようなエラーや外的要因がありえるかを考えてテストを行(引数のエラーから、メモリやディスク、ネットワークまで様々なケースがありえる)
- Performance(パフォーマンスの特性):
- パフォーマンスの問題においてボトルネックの検出は難しい
- UnitTestによるパフォーマンスの計測とアサーションはこういった検出において役立つ
- コードの変更の前後でパフォーマンスが劣化していないのチェックという効果もある
- 例:あるふるまいを1000回実行して、想定の実行時間内に収まっているかチェックする
- 注意点:
- タイミングの影響を受けないように大量に実行する
- 他のUnitTestよりも低速なので、切り離して1日1回だけ実行するなどの対応を行う
- 環境や負荷に応じて結果は変化するので、実行する条件は極力同一にする(答えは無いので、実環境と同じというのが一番の最適解)
第7章 CORRECT(境界条件の扱い)
前章のBoundaryで説明のあった境界条件の見つけ方として挙げられたCORRECTの各要素の説明がされている章です。
書かれいる内容はUnitTestだけに留まらず、一般的な画面操作などの上位のテストでも通じる内容が多いと思います。
- Conformance(適合):値は期待される形式に適合されていますか?
- 入力されるデータが形式に沿っていない場合のケースをテストとして追加する
- 例1;メールアドレスで@が無い場合
- 例2:レポートのレイアウトパターン(ヘッダフッタあり・なしのパターン)
- システムから何度も使われている情報は、入力時に1回だけ検証すれば良くテストケースを減らせる(例:口座情報はUIから入力時、APIの引数、ファイルから読み込んだ時に実施すればよい)
- そのためにもデータの流れの把握が必要
- Ordering(順序):値の集合は適切な順序で並び替えられていますか?(もしくは並び変わらないか?)
- コレクションの戻り値が正しい形式でソートされているかを確認する
- Range(範囲):値は最小値と最大値の間にありますか?
- Reference(参照):自身が直接コントロールできない外部のコードを参照していませんか?
- 参照先が範囲の外にある場合、外部への依存、特定の状態へのオブジェクトへの依存、必須の条件 といったケースを考慮したテストを行う
- 例1:ログインしていない状態で顧客の入出金明細を表示した場合
- 例2:変速機で、走行中であればPへのシフトチェンジを無視する、停止中のみPへ停止可能など
- メソッドの実行前の条件を考慮したケース及び、実行後の副作用(状態変更)を考慮したケースの考慮が必要である
- Existence(存在):値は存在しますか?(null,0,集合が空ではないないか?)
- Cardinality(要素数):十分な個数の値が用意されていますか?
- 0,1,複数個のケースで要素数を考慮してテストを実施する(複数個は大概のケースで1パターンで同じ振る舞いになる)
- 例:商品の売り上げTop10を表示するケース
- 結果の出力:(0,1,10個のケース)
- ランキングへの追加:(0->1, 1->2, 9->10, 10->10)
- Time(時間):すべての出来事は一定の順序で発生しますか?適切なタイミングで発生して、一定時間内で発生しますか?
- 内部状態を保持し、順序の考慮が必要な場合の対応のテスト (例:ログインの前にログアウト処理を実行したらどうなる?)
- タイムアウト、うるう年、うるう時間など
第8章 クリーンなコードを目指すリファクタリング
UnitTestのメリットとしてリファクタリングがあります。
内部の構造を変更しても、外部の振る舞いを保証するテストがあるため、安全にリファクタリングが可能になります。
本章では1つの冗長なクラスのメソッドを例に、UnitTestがある前提でどのようにリファクタリングを行えば良いか説明がされています。
出てくる考え方や手法は以前紹介したレガシーコード改善ガイドに通じるものがあります。
ループ数が増加してもコードの読みやすさを優先した方が、大概の場合は良いというのは、忘れがちになってしまうので覚えておきたいですね。(確かリーダブルコードにも書かれたかな?)
- リファクタリングとは => 機能面でのふるまいを変えずに内部の構造を変更する事
- 変更に伴うシステムの破壊を防ぐためにUnitTestが必要になる
- メソッドを抜き出す => 冗長な部分を意図のわかるメソッド名を付けて別の処理に置き換える
- メソッドの置き場を決める => 対象のオブジェクトと関係ない、別のオブジェクト間の処理であれば別のクラスに処理を移す
- 例:クラスA(テスト対象)と内部で使用されているクラスBとCがあって、Aの中でBとCを使用した処理をしているならばBもしくはCに処理を移す
- 過剰なリファクタリングの是非
- リファクタリングのせいで、ループ数が3倍になった場合は許される?
- パフォーマンスに関して特段の理由がなければ問題にはならない。コードをクリーンに保つ方が重要。
第9章 より大きな設計上の課題
前章は基本的に1つのメソッドの処理をクラス内でどのようにリファクタリングしていくか?といった内容がメインでした。
本章ではスコープを広げて、単一責任の原則に沿って、別のクラスに処理を移してリファクタリングしていくといった事を行なっていきます。
また、クラスの構造の変化に伴い、壊れてしまったユニットテストを修復する(新たに追加したクラスのユニットテストを実装する)といった実践的な例も含まれていて参考になります。
クラスを適切に分割して行く事で、コードをクリーンに保ちつつ変更に強い実装が可能になります。
- SRP(単一責任の原則)
- クラスの責務を1つに保つ事で変更のリスクを下げる
- 責務が小さくなる事でクラスの処理の再利用も可能になる
- コマンドとクエリの分離
- 戻り値を返すのと副作用(内部の状態を変更する)を生む処理は分離すべきという考え
第10章 モックオブジェクト
外部API呼び出し処理など、テストが困難であったり実行時間に影響を及ぼす処理を、テスト実行可能にするための手法である、スタブ・モックに関する方法をまとめている章です。
外部API呼び出しなどはモック、スタブ化するのは定番ですが、クラス感の結合部分に関してはどうすべきなのかちょっと気になる点です。(これまでのサンプルであれば複雑ではないのでモックを使用せずにそのままテストしている)
第11章 テストのリファクタリング
テスト対象のコードと合わせて、テストコード自体も機能変更や追加に伴い変更が必要になります。
本章では、テストコード自体の変更を最小限に抑えるためのリファクタリング手法が説明されています。
例題としてひどいテストコードを紹介し、そのテストコードを少しずつリファクタリングしていく方法が説明されています。
第12章 テスト駆動開発
これまでは既存のコードに対してどのようなテストを書くかといった視点での説明でしたが、本章ではテスト駆動開発(TDD)を用いて、テストからコードを作っていく方法を実際にやっていきます。
第13章 テストが難しい事柄
マルチスレッドとDBアクセスを例にUnitTestが難しいケースに対してどのように対応を行えば良いかを説明する章です。
マルチスレッドに関しては、関心の分離によりロジックとマルチスレッド処理を分割して個々にテストを行うといった方法を行い、DBに関しては実際にDBアクセスを行なった統合テスト的な観点でのテストを実装しています。
DBを使った統合テストに関しては、UnitTestではカバーできない部分をテストする上で大切ですが、実行時間や環境の問題などが発生するため取扱いが難しいなと改めて感じました。
- 関心の分離:スレッド、DBなどのアプリロジック以外の依存とアプリロジックを分離させる
- 必要ならば統合テストを作成しても良いが、目的を絞ったものにする
第14章 プロジェクトでのテスト
チームで開発していく上でUnitTestに関して、足並みを揃えるためにチームで合意しておくルールに関して説明がされています。
レビューやCD/CI、カバレッジ基準など他のユニットテスト本などでも耳にする内容に関して書かれています。
あくまで概要レベルの説明なので、実際の適応などは自身の環境への最適化が必要です。
- ユニットテストの基準を定める
- テストの命名ルール
- 使用するモックツール
- 低速なテストが見つかった場合に高速化を促す方針
- プルリクエストなどの仕組みでレビューで基準を遵守させる仕組みを作る
- カバレッジ
- 望ましいカバレッジの値:
- 100%を実現する事に意味はない => 無意味なテストケースを捏造しないと達成できない
- カバレッジが低くなる原因は、劣悪な依存関係によるテストが困難な場合が多い
- 良い設計になればテストが容易になり、自然にカバレッジが上がる(TDDならば90%を自然に超える)
- カバレッジを上げてもアサーションを正しく実装していないと意味はない
- 単体の値に意味は少ない、増減傾向を確認することが大切
- カバレッジの低いコードや減少傾向のチームに対してのみカバレッジの計測が役立つ
- 望ましいカバレッジの値: