よしたろうブログ

設計・人文知・歴史・哲学・漫画とかの話が好きです。

JUnitで学ぶユニットテストの実践的で本質的な考え方 〜前編〜

初めに

JUnit に触れたことがなく最近実務で触れ始めた自分用の記事です。実務歴一年過ぎた位です。 具体的なコードや方法も記述しますが、それよりも JUnitユニットテストの概念や目的など本質的なことをまず押さえてつつ並行して進めていきます。

こちらの記事のブラッシュアップ版として以下の記事を書きました。

こちらの方が私にとっては、より質の高い記事だと思ってますのでこちらをご覧いただけると幸いです。以下のような違いがあります。

追加・変更事項

ユニットテスト対象のシステム・コンポーネントの、依存先に対するスタンス 依存先をモック(テストダブル)で置換する際の線引き。前記事では基本的に全てをテストダブルで置換するスタンスで書きました。本記事ではできる限りテストダブルで置換しない、というスタンスで記事を再構成ています。 全体的にブラッシュアップし、より多方面をカバー 参考資料をより多く熟読し、様々な視点から記事を修正・変更・再構成・追加しました。 カバレッジリファクタリングについて ユニットテストリファクタリングについて モックなどのテストダブルについて FIRST の概念をE2Eなどの考え方にまで拡張 etc....

環境

  • EclipseJUnit 5 を構成 ビルドシステム(Maven / Gradle)を使用しておらず、新しいテストの作成時に JUnit ライブラリがプロジェクトのクラスパスに含まれていない場合、追加するように求められるので追加します。junit-jupiterなど
  • intelliJ IDEA / java17 / SpringBoot / junit5 / Mac

※ビルドシステム(Maven / Gradle)を使用したい場合、こちらを参考にしてください。

maven を使用する場合は以下の plugin(2.22.0以上)が必要です(SpringBootはデフォルトで定義あり)

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>2.22.2</version>
        </plugin>
        ...
     </plugins>
</build>

1. 概要

JUnitJava 言語向けのユニットテストフレームワークです。

1997年に、Smalltalk のためのユニットテストフレームワークであるSUnitをもとにして、エーリヒ・ガンマと、SUnitの開発者のケント・ベックが中心となって開発されました。飛行機の中でペアプロして作っちゃったというエピソードがあります。

JUnitは最も有名なオープンソースユニットテストフレームワークで、Javaアプリケーション開発者にとって非常に有用なツール。 JUnitは、シンプルなアサーションを使用して、開発者がコードが期待どおりに動作するかどうかをテストすることを可能にしています。 JUnitを使用することで、開発者は、コードの品質を高めることができ、エラーを早期に検出して修正することができるようになります。

JUnitは、多くのIDEがデフォルトでサポートしていて、EclipseNetBeansIntelliJなどのIDEでは標準装備です。

具体的には、Javaのプログラムをテストするためのテストケースを定義し、テストケースを実行して、テスト結果を把握することができます。 JUnitを使用することで、開発者は、変更を加えたコードが思わしくない動作をしないことを保証できるようになります。

2. ソフトウェアテストの目的

ソフトウェアテストとは、あるソフトウェアを実行し、そのソフトウェアが特定の方法で動作することを保証するものです。

ソフトウェアテストによって、ソフトウェアのある部分が期待通りに動作することを保証します。これらのテストは通常、ビルドシステムを通じて自動的に実行されるため、開発者が開発活動中に既存のコードを壊さないようにするために役立ちます。

テストを自動的に実行することで、ソースコードの変更によって生じたソフトウェアのデグレード(修正したバグや不具合が復活したり、ソフトウェアのバージョンアップで機能が低下したりすること)を特定することができます。コードのテストカバレッジが高ければ、多くの手動テストを実行することなく、機能開発を継続することができます。

そもそも、なぜテストが必要かというと、プログラムは人間が書きます。そして人間は間違いを犯します。つまり、プログラムは間違っていることが前提となるわけでけです。そして、プログラムの間違いは人や企業に損害を与えることがあります。例えば、業務の中断・人手による代替・損害賠償・企業イメージ低下・人の財産や生命の損害、などなどが考えられます。なので、それらの損害を未然に防ぐために、リリース前に間違いを検知修正する必要がります。それがテストです。

テストには以下の種類があります。

  • 単体テスト
    • 各クラスやメソッドがちゃんと動作するか確認する
  • 結合テスト
    • いくつかのクラスをつなげてテストする(機能単位など)
  • システムテスト
    • システム全体のテスト
    • パフォーマンスやセキュリティのテストも行う

2-1. 単体(ユニット)テスト

単体テストとは、開発者が書いたコードの一部で、テスト対象のコードの特定の機能を実行し、特定の動作や状態を保証するものです。ユニットテストによってテストされるコードの割合は、一般的にテストカバレッジと呼ばれます。依存関係をテストの実装やテストフレームワークで作成された(モック)オブジェクトに置き換えることで、外部依存性をユニットテストから取り除くことができます。

ユニットテストは、コードの小さな単位、例えば、メソッドやクラスを対象とするため、複雑なユーザインターフェイスコンポーネントの相互的なやりとりをテストするのには適していません。これについては、統合テストを開発する必要があります。

ユニットテストの特徴を簡単に紹介します(詳細は「2-5. JUnit の特徴と問題点」で紹介)

  • プログラムとして実行できる仕様書となる(仕様を保証する)
  • プログラムの修正のたびに繰り返し実施する

Junitなどのフレームワークを使用すれば

  • テストの実行
  • 検証
  • テスト結果のレポートetc・・・

といった、テストケースとは直接関連しない面倒な部分を実装する必要がなくなります。フレームワークを使用することで、テストケースの設計と実装に専念できるようになるわけです。

2-1-1. ユニットテストを作成するべきタイミングと、その理由

  • 何らかの機能のコーディングが完了し、期待どおりにふるまうかどうか確認したい場合
  • コードへの変更内容を記録し、自分や他の開発者が変更の意図を理解できるようにしたい場合
  • コードを変更する必要があり、その際に既存の機能を損ねないようにしたい場合
  • 現在のシステムのふるまいについて理解したい場合
  • 他者によるコードが期待どおりに機能しなくなった際に、それがいつからなのか知りたい場合

何よりも重要なのは、よいユニットテストを行えば自信を持って実運用向けのシステムをリリースできるという点です。

2-2. 結合(統合)テスト

結合テストコンポーネントの挙動、もしくはコンポーネントの集合の間の統合をテストすることを目的としています。機能テストという用語は、結合テストの同義語として使われることもあります。結合テストは、システム全体が意図したとおりに動作することを確認するものであり、したがって、集中的な手動テストの必要性を減らすことができます。これらの種類のテストは、ユーザーストーリーをテストスイート(テストの実行単位)に変換することを可能にします。テストはアプリケーションに対する期待されるユーザーのインタラクションな動作に似るでしょう。

2-3. 性能テスト

性能テストは、ソフトウェアコンポーネントを繰り返しベンチマークするために使われます。その目的は、テスト対象のコードが高負荷状態であっても十分に高速に動作することを確認することです。

3. JUnit単体テスト用のフレームワーク

単体テストは最も低いレベルでのソフトウェアテストです。Javaの世界の単体テストでは、通常、特定のメソッドが正しい結果を返すかどうかをチェックします。

すべての優れたソフトウェアには単体テストが必要であり、すべてのソフトウェア開発者は単体テストを書くべきとの意見がよく見られます。単体テストは、開発中の信頼性を高め、よりクリーンで再利用可能なコードを書くことに繋がるからです。

クラスの単体テストを書こうとしてそれができない場合は、クラス設計が間違っている可能性があります。よく言われるのは、DIP(依存性逆転原則)が適切に適用されていない場合などです。JUnitで行うテストはユニットの独立性が前提となります。ここに関しては「7. 良いユニットテストであるための原則『FIRST』」にて解説します。

正しい単体テストはミスのコストを低減します。バグを修正するのは、アプリケーションがバグを検出するときよりも、開発の早い段階で検出した方がはるかに簡単です。単体テストは通常、ソフトウェアではなく、新しい機能を作成する際にソフトウェア開発者によって作成されます。以下の画像は、いわゆるテストピラミッドを示しています。

ピラミッドの下位から上位に向かうにしたがってコストがかさむことを表しています。幅がテストケースの実行量を表現し、より下位のテストにテストケースを移動した方がコストの改善に繋がることを意味します。 テストを充実させるためには、テストをこのピラミッドの下位へ寄せていくように工夫する必要があります。

スクリーンショット 2023-02-04 17.41.17.png

JUnit には 4と5というヴァージョンがありますが、互換性はありません。使い方・アーキテクチャも全て異なります。ただ、4上で5のテストを動かしたり、 5上で4のテストを動かしたりすることは可能ではあります。

現在、新規に JUnit を導入する場合は5を一択と言われています。4のサポートがいつ切れるかわかりません。

3-1. JUnitホワイトボックステスト

仕様を元に準備値と期待結果のみをテストする

ブラックボックステストは、テスト対象の仕様を元に動作を確認します。テスト対象のロジックそのものの内容を知る必要がありません。渡した引数に対しての戻り値がなんであるかが解れば、実装の詳細について知る必要がありません。

仕様を元に準備値から期待結果までのロジックもテストする

対してホワイトボックステストは、テスト対象のロジックそのものに対してテストを行うため、実装の詳細について知る必要があります。実装の詳細・ロジックが正しいかどうかをテストするためです。

3-2. ホワイトボックステストの実行手順

  1. 実装されているプログラムを分析する
  2. プログラム中のパス (実行経路)を把握する
    1. if などの条件分岐でプログラムの実行経路が何パターンあるか?
  3. 試験を行うパスを決定する
  4. 指定したパスを通るような条件を求める
  5. 試験を実施する
    1. 実行経路のパターン数分実施する
  6. 試験結果と期待値を比較する
  7. テスト対象が意図通りの動作を行っているか判定する

3−3. カバレッジとテスト網羅方法

カバレッジとはテスト網羅率のことです。ソフトウェアテスト進捗を表す尺度。テスト対象ソースコードのうち、どの程度の割合のコードがテストされたかを表します。適当なテスト内容でもテストを実施していればカバレッジを上げることはできます。必ずしも数字が高ければコードの品質が高い、ということに直結しない場合もあるので盲信には注意が必要です。

カバレッジには以下の特徴があります。

  1. 「適切な境界値分析ができていなくても検出できない」
    テスト時に実装されている処理・条件で通っていれば100%になる
  2. 「仕様に対して実装が適切かどうかは判断できない」
    テスト時に実装されている全処理・全条件を通っていれば100%になる

結局は仕様をコードで実現できてるかどうかが重要であり、カバレッジはそういった前提でなければ意味のない指標になります。カバレッジが高すぎる場合は、逆に疑う方が良いかもしれません。テストコードに実装を合わせたのではなく、実装にテストコードを合わせたのかもしれません。もちろん、全く無駄なテストになりますね。かといって、「カバレッジが低い=品質が低い」は直結するのは疑う余地がありません。

3-4. JUnit の特徴と問題点

特徴

  • 一度作成すればすばやくテスト可能である。
  • その後はテストコードを標本とすることでバグ訂正が容易となる。
  • テストコードを見れば仕様が一目瞭然となる。
  • 誰でも同じテストを行えるようになる。
  • 独自のテストコードによるテスト作成の手間を省ける。

既存のコードに変更を加えてもバグの検知が用意であるため、機能追加・リファクタリングを行い易くなります。それを大きな目的の一つとして、テストの自動化があるといってもいいかもしれません。 プログラムの変更コストやリスクが小さくなれば、市場の変化に追随し易くなることはもちろん、ユーザーのニーズに対しても素早く対応できる様になるでしょう。企業の競争力の増加に直結するといっても過言では無いでしょう。

人力のテストなんて無駄かつ意味なしです(エクセルにスクショペタペタ作業、憎しみ)

手動テストでは、テストの実行に多くの時間と人件費が発生する上、それでもバグは検知しきれません。認知能力を超えた規模に人力では立ち向かえません。また、機能追加や修正などのプログラム変更に莫大なコストと時間が発生し、競争力が低下することになります。

問題点

  1. 仕様変更ごとにテストコードを作り直さなければならない。
    • IDEを使うことで、テストコードの再作成によって生じる手間を軽減することもできる。
  2. テストコードの作成に時間がかかる。
    • IDEを使うことでテストコードの作成を高速化することはできる。

4. 基本的なルール

4-1. テストクラスのルール

  • テスト対象となるクラスに対し、対になるテストクラスを作成する
  • 作成するテストクラスのクラス名は「テスト対象クラス名 + Test」とする
  • テスト対象クラスとテストクラスのパッケージは同一構造にする
    • これには方法が三つある(「5. テストクラスと対象クラスの分離」にて解説)

4-2. テストメソッドのルール

  • @Test アノテーションを付与する
  • 戻り値はvoid(戻り値なし)とする
  • 引数は持たせない
  • 各assertメソッドで期待値と実際値を検証する

4-3. @Testの注意点

  • JUnit 5と4でパッケージ名が異なる
  • JUnit 5:org.junit.jupiter.api.Test
  • JUnit 4:org.junit.Test
  • 期待する動作と異なる場合、 import文を確認する

続き

以下が後編です。

ここまでは基礎の基礎でしたが、より込み入った実践的な話になります。