堅牢なユニットとJUnitとの統合テストのガイド
公開: 2022-03-11自動化されたソフトウェアテストは、ソフトウェアプロジェクトの長期的な品質、保守性、および拡張性にとって非常に重要です。Javaの場合、JUnitは自動化への道です。
この記事のほとんどは、堅牢な単体テストの作成とスタブ、モック、依存性注入の利用に焦点を当てていますが、JUnitと統合テストについても説明します。
JUnitテストフレームワークは、Javaベースのプロジェクトをテストするための一般的な無料のオープンソースツールです。
この記事の執筆時点で、JUnit 4は現在のメジャーリリースであり、10年以上前にリリースされており、最後の更新は2年以上前です。
JUnit 5(Jupiterプログラミングおよび拡張モデルを使用)は活発に開発されています。 Java 8で導入された言語機能をより適切にサポートし、他の新しい興味深い機能が含まれています。 一部のチームはJUnit5を使用する準備ができていると感じるかもしれませんが、他のチームは5が正式にリリースされるまでJUnit4を使い続けるかもしれません。 両方の例を見ていきます。
JUnitの実行
JUnitテストはIntelliJで直接実行できますが、Eclipse、NetBeans、さらにはコマンドラインなどの他のIDEでも実行できます。
テスト、特に単体テストは、常にビルド時に実行する必要があります。 問題が本番環境にあるかテストコードにあるかに関係なく、テストが失敗したビルドは失敗したと見なす必要があります。これには、チームからの規律と、失敗したテストの解決を最優先する意欲が必要ですが、自動化の精神。
JUnitテストは、Jenkinsのような継続的インテグレーションシステムによって実行およびレポートすることもできます。 Gradle、Maven、Antなどのツールを使用するプロジェクトには、ビルドプロセスの一部としてテストを実行できるという追加の利点があります。
Gradle
JUnit 5のサンプルGradleプロジェクトとして、JUnitユーザーガイドのGradleセクションとjunit5-samples.gitリポジトリを参照してください。 JUnit 4 API( 「ヴィンテージ」と呼ばれる)を使用するテストも実行できることに注意してください。
プロジェクトは、メニューオプション[ファイル]>[開く...]> junit-gradle-consumer sub-directory
移動>[OK]>[プロジェクトとして開く]>[OK]を選択して、IntelliJで作成できます。Gradleからプロジェクトをインポートします。
Eclipseの場合、BuildshipGradleプラグインは[ヘルプ]>[Eclipse Marketplace]からインストールできます。プロジェクトは、[ファイル]>[インポート]…>[Gradle]>[Gradleプロジェクト]>[次へ]>[次へ]>[ junit-gradle-consumer
サブディレクトリ]>[次へ]でインポートできます。 >次へ>終了。
IntelliJまたはEclipseでGradleプロジェクトをセットアップした後、Gradle build
タスクを実行すると、 test
タスクを使用してすべてのJUnitテストを実行する必要があります。 コードに変更が加えられていない場合、 build
の後続の実行でテストがスキップされる可能性があることに注意してください。
JUnit 4については、GradlewikiでのJUnitの使用を参照してください。
Maven
JUnit 5の場合、Mavenプロジェクトの例については、ユーザーガイドのMavenセクションとjunit5-samples.gitリポジトリを参照してください。 これは、ビンテージテスト(JUnit 4 APIを使用するテスト)も実行できます。
IntelliJで、[ファイル]>[開く...]>[ junit-maven-consumer/pom.xml
移動>[OK]>[プロジェクトとして開く]を使用します。 その後、テストはMavenプロジェクト>junit5-maven-consumer>ライフサイクル>テストから実行できます。
Eclipseで、「ファイル」>「インポート…」>「Maven」>「既存のMavenプロジェクト」>「次へ」>「 junit-maven-consumer
ディレクトリー」を参照>「 pom.xml
」を選択して>「終了」を使用します。
Mavenビルドとしてプロジェクトを実行することでテストを実行できます…> test
の目標を指定>実行します。
JUnit 4については、MavenリポジトリのJUnitを参照してください。
開発環境
GradleやMavenなどのビルドツールを介してテストを実行することに加えて、多くのIDEはJUnitテストを直接実行できます。
IntelliJ IDEA
JUnit5テストにはIntelliJIDEA2016.2以降が必要ですが、JUnit4テストは古いバージョンのIntelliJで機能するはずです。
この記事の目的上、GitHubリポジトリの1つ(JUnit5IntelliJ.gitまたはJUnit4IntelliJ.git)からIntelliJで新しいプロジェクトを作成することをお勧めします。これには、単純なPerson
クラスの例のすべてのファイルが含まれ、組み込みのファイルが使用されます。 JUnitライブラリ。 テストは、「実行」>「実行」「すべてのテスト」で実行できます。 テストは、 PersonTest
クラスからIntelliJで実行することもできます。
これらのリポジトリは、新しいIntelliJ Javaプロジェクトで作成され、ディレクトリ構造src/main/java/com/example
およびsrc/test/java/com/example
を構築します。 src/main/java
ディレクトリがソースフォルダとして指定され、 src/test/java
がテストソースフォルダとして指定されました。 @Test
で注釈が付けられたテストメソッドを使用してPersonTest
クラスを作成した後、コンパイルに失敗する場合があります。その場合、IntelliJは、IntelliJIDEAディストリビューションからロードできるクラスパスにJUnit4またはJUnit5を追加することを提案します(これらを参照)詳細については、Stack Overflowで回答してください)。 最後に、すべてのテストにJUnit実行構成が追加されました。
IntelliJテストのハウツーガイドラインも参照してください。
Eclipse
Eclipseの空のJavaプロジェクトには、テストルートディレクトリがありません。 これは、プロジェクトのプロパティ>Javaビルドパス>フォルダの追加…>新しいフォルダの作成…>フォルダ名の指定>終了から追加されました。 新しいディレクトリがソースフォルダとして選択されます。 残りの両方のダイアログで[OK]をクリックします。
JUnit 4テストは、[ファイル]>[新規]>[JUnitテストケース]で作成できます。 「NewJUnit4test」を選択し、新しく作成したテスト用のソースフォルダーを選択します。 「テスト対象クラス」と「パッケージ」を指定し、パッケージがテスト対象クラスと一致することを確認します。 次に、テストクラスの名前を指定します。 ウィザードの終了後、プロンプトが表示されたら、ビルドパスに「JUnit4ライブラリを追加する」を選択します。 プロジェクトまたは個々のテストクラスは、JUnitテストとして実行できます。 Eclipseの書き込みとJUnitテストの実行も参照してください。
NetBeans
NetBeansは、JUnit4テストのみをサポートします。 テストクラスは、NetBeans Javaプロジェクトで、[ファイル]>[新しいファイル]…>[単体テスト]>[JUnitテスト]または[既存のクラスのテスト]を使用して作成できます。 デフォルトでは、テストルートディレクトリはプロジェクトディレクトリでtest
という名前になっています。
単純な本番クラスとそのJUnitテストケース
非常に単純なPerson
クラスの本番コードとそれに対応する単体テストコードの簡単な例を見てみましょう。 サンプルコードを私のgithubプロジェクトからダウンロードして、IntelliJ経由で開くことができます。
src / main / java / com / example / Person.java
package com.example; class Person { private final String givenName; private final String surname; Person(String givenName, String surname) { this.givenName = givenName; this.surname = surname; } String getDisplayName() { return surname + ", " + givenName; } }
不変のPerson
クラスには、コンストラクターとgetDisplayName()
メソッドがあります。 getDisplayName()
が期待どおりにフォーマットされた名前を返すことをテストしたいと思います。 単一単体テスト(JUnit 5)のテストコードは次のとおりです。
src / test / java / com / example / PersonTest.java
package com.example; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class PersonTest { @Test void testGetDisplayName() { Person person = new Person("Josh", "Hayden"); String displayName = person.getDisplayName(); assertEquals("Hayden, Josh", displayName); } }
PersonTest
は、JUnit5の@Test
とアサーションを使用します。 JUnit 4の場合、 PersonTest
クラスとメソッドはパブリックである必要があり、異なるインポートを使用する必要があります。 これがJUnit4の例の要点です。
IntelliJでPersonTest
クラスを実行すると、テストに合格し、UIインジケーターが緑色になります。
一般的なJUnitの規則
ネーミング
必須ではありませんが、テストクラスの命名には一般的な規則を使用します。 具体的には、テスト対象のクラスの名前( Person
)から始めて、それに「Test」を追加します( PersonTest
)。 テストメソッドの命名も同様で、テスト対象のメソッド( getDisplayName()
)から始まり、それに「test」を追加します( testGetDisplayName()
)。 テストメソッドの命名には他にも完全に受け入れられる規則がたくさんありますが、チームとプロジェクト全体で一貫していることが重要です。
本番環境での名前 | テストでの名前 |
---|---|
人 | 人のテスト |
getDisplayName() | testDisplayName() |
パッケージ
また、本番コードのPerson
クラスと同じパッケージ( com.example
)にテストコードPersonTest
クラスを作成するという規則を採用しています。 テストに別のパッケージを使用した場合、単体テストによって参照される本番コードクラス、コンストラクター、およびメソッドでパブリックアクセス修飾子を使用する必要があります。これは、適切でない場合でも、同じパッケージに保持することをお勧めします。 。 ただし、リリースされた本番ビルドにテストコードを含めたくないため、個別のソースディレクトリ( src/main/java
とsrc/test/java
)を使用します。
構造と注釈
@Test
アノテーション(JUnit 4/5)は、 testGetDisplayName()
メソッドをテストメソッドとして実行し、合格か不合格かを報告するようにJUnitに指示します。 すべてのアサーション(存在する場合)が合格し、例外がスローされない限り、テストは合格と見なされます。
テストコードは、Arrange-Act-Assert(AAA)の構造パターンに従います。 その他の一般的なパターンには、Given-When-ThenおよびSetup-Exercise-Verify-Teardown(通常、単体テストではティアダウンは明示的に必要ありません)が含まれますが、この記事ではAAAを使用します。
テスト例がAAAにどのように準拠しているかを見てみましょう。 最初の行である「arrange」は、テストされるPerson
オブジェクトを作成します。
Person person = new Person("Josh", "Hayden");
2行目の「act」は、プロダクションコードのPerson.getDisplayName Person.getDisplayName()
メソッドを実行します。
String displayName = person.getDisplayName();
3行目の「assert」は、結果が期待どおりであることを確認します。
assertEquals("Hayden, Josh", displayName);
内部的には、 assertEquals()
呼び出しは、「Hayden、Josh」文字列オブジェクトのequalsメソッドを使用して、本番コード( displayName
)から返された実際の値が一致することを確認します。 一致しなかった場合、テストは失敗とマークされます。
多くの場合、テストにはこれらのAAAフェーズごとに複数の行があることに注意してください。
ユニットテストとプロダクションコード
いくつかのテスト規則について説明したので、本番コードをテスト可能にすることに注意を向けましょう。
Person
クラスに戻ります。ここでは、誕生日に基づいて年齢を返すメソッドを実装しました。 コード例では、Java8が新しい日付と機能的なAPIを利用する必要があります。 新しいPerson.java
クラスは次のようになります。
Person.java
// ... class Person { // ... private final LocalDate dateOfBirth; Person(String givenName, String surname, LocalDate dateOfBirth) { // ... this.dateOfBirth = dateOfBirth; } // ... long getAge() { return ChronoUnit.YEARS.between(dateOfBirth, LocalDate.now()); } public static void main(String... args) { Person person = new Person("Joey", "Doe", LocalDate.parse("2013-01-12")); System.out.println(person.getDisplayName() + ": " + person.getAge() + " years"); // Doe, Joey: 4 years } }
このクラスを実行すると(執筆時点で)、Joeyが4歳であることがわかります。 テストメソッドを追加しましょう:
PersonTest.java
// ... class PersonTest { // ... @Test void testGetAge() { Person person = new Person("Joey", "Doe", LocalDate.parse("2013-01-12")); long age = person.getAge(); assertEquals(4, age); } }
今日は過ぎますが、1年後の実行はどうでしょうか。 期待される結果はテストを実行しているシステムの現在の日付に依存するため、このテストは非決定論的で脆弱です。
バリューサプライヤーのスタブと注入
本番環境で実行する場合、現在の日付LocalDate.now()
を使用して個人の年齢を計算しますが、1年後でも確定的なテストを行うには、テストで独自のcurrentDate
値を指定する必要があります。
これは依存性注入として知られています。 Person
オブジェクトが現在の日付自体を決定するのではなく、このロジックを依存関係として渡します。 単体テストでは既知のスタブ値が使用され、実動コードでは実行時にシステムから実際の値を提供できます。
LocalDate
サプライヤーをPerson.java
に追加しましょう:
Person.java
// ... class Person { // ... private final LocalDate dateOfBirth; private final Supplier<LocalDate> currentDateSupplier; Person(String givenName, String surname, LocalDate dateOfBirth) { this(givenName, surname, dateOfBirth, LocalDate::now); } // Visible for testing Person(String givenName, String surname, LocalDate dateOfBirth, Supplier<LocalDate> currentDateSupplier) { // ... this.dateOfBirth = dateOfBirth; this.currentDateSupplier = currentDateSupplier; } // ... long getAge() { return ChronoUnit.YEARS.between(dateOfBirth, currentDateSupplier.get()); } public static void main(String... args) { Person person = new Person("Joey", "Doe", LocalDate.parse("2013-01-12")); System.out.println(person.getDisplayName() + ": " + person.getAge() + " years"); // Doe, Joey: 4 years } }
getAge()
メソッドのテストを容易にするために、現在の日付を取得するためにLocalDate
サプライヤーであるcurrentDateSupplier
を使用するように変更しました。 サプライヤーが何かわからない場合は、Lambda組み込み関数型インターフェースについて読むことをお勧めします。
また、依存性注入を追加しました。新しいテストコンストラクターを使用すると、テストで独自の現在の日付値を提供できます。 元のコンストラクターはこの新しいコンストラクターを呼び出し、 LocalDate::now
の静的メソッド参照を渡します。これはLocalDate
オブジェクトを提供するため、メインメソッドは以前と同じように機能します。 私たちのテスト方法はどうですか? PersonTest.java
を更新しましょう:
PersonTest.java
// ... class PersonTest { // ... @Test void testGetAge() { LocalDate dateOfBirth = LocalDate.parse("2013-01-02"); LocalDate currentDate = LocalDate.parse("2017-01-17"); Person person = new Person("Joey", "Doe", dateOfBirth, ()->currentDate); long age = person.getAge(); assertEquals(4, age); } }
テストは独自のcurrentDate
値を注入するようになったため、来年または任意の年に実行した場合でも、テストは合格します。 これは一般にスタブと呼ばれるか、返される既知の値を提供しますが、最初に、この依存関係を注入できるようにPerson
を変更する必要がありました。
Person
オブジェクトを作成するときは、ラムダ構文( ()->currentDate
)に注意してください。 これは、新しいコンストラクターで必要とされるように、 LocalDate
のサプライヤーとして扱われます。
Webサービスのモックとスタブ
これで、 Person
オブジェクト(その存在全体がJVMメモリに存在する)が外部と通信する準備が整いました。 2つのメソッドを追加します。1つは人物の現在の年齢を投稿するpublishAge()
()メソッドで、もう1つはPerson
と同じ誕生日または同じ年齢の有名人の名前を返すgetThoseInCommon()
メソッドです。 「PeopleBirthdays」と呼ばれる、対話できるRESTfulサービスがあるとします。 単一のクラスBirthdaysClient
で構成されるJavaクライアントがあります。
com.example.birthdays.BirthdaysClient
package com.example.birthdays; import java.io.IOException; import java.util.Arrays; import java.util.Collection; public class BirthdaysClient { public void publishRegularPersonAge(String name, long age) throws IOException { System.out.println("publishing " + name + "'s age: " + age); // HTTP POST with name and age and possibly throw an exception } public Collection<String> findFamousNamesOfAge(long age) throws IOException { System.out.println("finding famous names of age " + age); return Arrays.asList(/* HTTP GET with age and possibly throw an exception */); } public Collection<String> findFamousNamesBornOn(int month, int dayOfMonth) throws IOException { System.out.println("finding famous names born on day " + dayOfMonth + " of month " + month); return Arrays.asList(/* HTTP GET with month and day and possibly throw an exception */); } }
Person
クラスを強化しましょう。 まず、 publishAge()
の望ましい動作のための新しいテストメソッドを追加します。 機能ではなく、なぜテストから始めるのですか? テスト駆動開発(TDDとも呼ばれます)の原則に従っています。最初にテストを記述し、次にテストに合格するためのコードを記述します。
PersonTest.java
// … class PersonTest { // … @Test void testPublishAge() { LocalDate dateOfBirth = LocalDate.parse("2000-01-02"); LocalDate currentDate = LocalDate.parse("2017-01-01"); Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate); person.publishAge(); } }
この時点で、 publishAge()
メソッドが作成されていないため、テストコードのコンパイルに失敗します。 空のPerson.publishAge()
メソッドを作成すると、すべてが通過します。 これで、その人の年齢が実際にBirthdaysClient
に公開されることを確認するためのテストの準備が整いました。

モックオブジェクトの追加
これは単体テストであるため、高速かつメモリ内で実行する必要があります。したがって、テストはモックのBirthdaysClient
を使用してPerson
オブジェクトを構築し、実際にWebリクエストを行わないようにします。 次に、テストはこのモックオブジェクトを使用して、期待どおりに呼び出されたことを確認します。 これを行うには、モックオブジェクトを作成するためのMockitoフレームワーク(MITライセンス)への依存関係を追加してから、モックされたBirthdaysClient
オブジェクトを作成します。
PersonTest.java
// ... import com.example.birthdays.BirthdaysClient; // ... import static org.mockito.Mockito.mock; class PersonTest { private BirthdaysClient birthdaysClient = mock(BirthdaysClient.class); // ... @Test void testPublishAge() { // ... Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient); // ... } }
さらに、 Person
コンストラクターのシグネチャを拡張してBirthdaysClient
オブジェクトを取得し、テストを変更して、モックされたBirthdaysClient
オブジェクトを挿入しました。
模擬期待を追加する
次に、 testPublishAge
の最後に、 BirthdaysClient
が呼び出されるという期待値を追加します。 新しいPersonTest.java
に示すように、 Person.publishAge()
はそれを呼び出す必要があります。
PersonTest.java
// ... class PersonTest { // ... @Test void testPublishAge() throws IOException { // ... Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient); verifyZeroInteractions(birthdaysClient); person.publishAge(); verify(birthdaysClient).publishRegularPersonAge("Joe Sixteen", 16); } }
Mockitoで強化されたBirthdaysClient
は、そのメソッドに対して行われたすべての呼び出しを追跡します。これにより、publishAge()を呼び出す前に、 publishAge()
verifyZeroInteractions()
メソッドを使用してBirthdaysClient
に対して呼び出されていないことを確認します。 間違いなく必要ではありませんが、これを行うことで、コンストラクターが不正な呼び出しを行わないようにします。 verify()
行で、 BirthdaysClient
への呼び出しがどのように見えるかを指定します。
publishRegularPersonAgeのシグネチャにはIOExceptionが含まれているため、テストメソッドのシグネチャにも追加することに注意してください。
この時点で、テストは失敗します。
Wanted but not invoked: birthdaysClient.publishRegularPersonAge( "Joe Sixteen", 16L ); -> at com.example.PersonTest.testPublishAge(PersonTest.java:40)
テスト駆動開発をフォローしているため、 Person.java
に必要な変更をまだ実装していないことを考えると、これは予想されることです。 次に、必要な変更を加えて、このテストに合格します。
Person.java
// ... class Person { // ... private final BirthdaysClient birthdaysClient; Person(String givenName, String surname, LocalDate dateOfBirth) { this(givenName, surname, dateOfBirth, LocalDate::now, new BirthdaysClient()); } // Visible for testing Person(String givenName, String surname, LocalDate dateOfBirth, Supplier<LocalDate> currentDateSupplier, BirthdaysClient birthdaysClient) { // ... this.birthdaysClient = birthdaysClient; } // ... void publishAge() { String nameToPublish = givenName + " " + surname; long age = getAge(); try { birthdaysClient.publishRegularPersonAge(nameToPublish, age); } catch (IOException e) { // TODO handle this! e.printStackTrace(); } } }
例外のテスト
プロダクションコードコンストラクターが新しいBirthdaysClient
をインスタンス化するようにし、 publishAge()
がbirthdaysClient
を呼び出すようになりました。 すべてのテストに合格します。 すべてが緑です。 すごい! ただし、 publishAge()
がIOExceptionを飲み込んでいることに注意してください。 バブルアウトさせるのではなく、 PersonException.java
という新しいファイルで独自のPersonExceptionでラップしたいと思います。
PersonException.java
package com.example; public class PersonException extends Exception { public PersonException(String message, Throwable cause) { super(message, cause); } }
このシナリオをPersonTest.java
の新しいテストメソッドとして実装します。
PersonTest.java
// ... class PersonTest { // ... @Test void testPublishAge_IOException() throws IOException { LocalDate dateOfBirth = LocalDate.parse("2000-01-02"); LocalDate currentDate = LocalDate.parse("2017-01-01"); Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient); IOException ioException = new IOException(); doThrow(ioException).when(birthdaysClient).publishRegularPersonAge("Joe Sixteen", 16); try { person.publishAge(); fail("expected exception not thrown"); } catch (PersonException e) { assertSame(ioException, e.getCause()); assertEquals("Failed to publish Joe Sixteen age 16", e.getMessage()); } } }
Mockito doThrow()
は、 publishRegularPersonAge()
メソッドが呼び出されたときに例外をスローするために、スタブbirthdaysClient
を呼び出します。 PersonException
がスローされない場合、テストは失敗します。 それ以外の場合は、例外がIOExceptionと適切にチェーンされていることを表明し、例外メッセージが期待どおりであることを確認します。 現在、本番コードに処理を実装していないため、予期された例外がスローされなかったため、テストは失敗します。 テストに合格するには、 Person.java
で変更する必要があるものは次のとおりです。
Person.java
// ... class Person { // ... void publishAge() throws PersonException { // ... try { // ... } catch (IOException e) { throw new PersonException("Failed to publish " + nameToPublish + " age " + age, e); } } }
スタブ:いつとアサーション
ここで、 Person.getThoseInCommon()
メソッドを実装して、 Person.Java
クラスを次のようにします。
testGetThoseInCommon()
は、 testPublishAge()
()とは異なり、 birthdaysClient
メソッドに対して特定の呼び出しが行われたことを確認しません。 代わりに、 getThoseInCommon()
が行う必要のあるfindFamousNamesOfAge findFamousNamesOfAge()
およびfindFamousNamesBornOn()
)の呼び出しのスタブ戻り値を呼び出すwhen
に使用します。 次に、指定した3つのスタブ名がすべて返されることを表明します。
assertAll()
JUnit 5メソッドで複数のアサーションをラップすると、最初に失敗したアサーションの後で停止するのではなく、すべてのアサーションを全体としてチェックできます。 また、含まれていない特定の名前を識別するために、 assertTrue()
を含むメッセージを含めます。 「ハッピーパス」(理想的なシナリオ)のテスト方法は次のようになります(これは「ハッピーパス」であるという性質上、堅牢な一連のテストではありませんが、その理由については後で説明します。
PersonTest.java
// ... class PersonTest { // ... @Test void testGetThoseInCommon() throws IOException, PersonException { LocalDate dateOfBirth = LocalDate.parse("2000-01-02"); LocalDate currentDate = LocalDate.parse("2017-01-01"); Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient); when(birthdaysClient.findFamousNamesOfAge(16)).thenReturn(Arrays.asList("JoeFamous Sixteen", "Another Person")); when(birthdaysClient.findFamousNamesBornOn(1, 2)).thenReturn(Arrays.asList("Jan TwoKnown")); Set<String> thoseInCommon = person.getThoseInCommon(); assertAll( setContains(thoseInCommon, "Another Person"), setContains(thoseInCommon, "Jan TwoKnown"), setContains(thoseInCommon, "JoeFamous Sixteen"), ()-> assertEquals(3, thoseInCommon.size()) ); } private <T> Executable setContains(Set<T> set, T expected) { return () -> assertTrue(set.contains(expected), "Should contain " + expected); } // ... }
テストコードをクリーンに保つ
見過ごされがちですが、テストコードを重複させないようにすることも同様に重要です。 クリーンなコードと「自分を繰り返さない」などの原則は、高品質のコードベース、本番コード、テストコードを同様に維持するために非常に重要です。 いくつかのテストメソッドがあるため、最新のPersonTest.javaに重複があることに注意してください。
これを修正するために、いくつかのことを行うことができます。
IOExceptionオブジェクトをプライベートfinalフィールドに抽出します。
ほとんどのPersonオブジェクトは同じパラメーターで作成されているため、
Person
オブジェクトの作成を独自のメソッド(この場合はcreateJoeSixteenJan2()
に抽出します。スローされた
PersonExceptions
を検証するさまざまなテスト用のassertCauseAndMessage()
を作成します。
クリーンなコードの結果は、PersonTest.javaファイルのこのレンディションで確認できます。
ハッピーパス以上のテスト
Person
オブジェクトの生年月日が現在の日付よりも遅い場合はどうすればよいですか? アプリケーションの欠陥は、多くの場合、予期しない入力、またはコーナー、エッジ、または境界のケースへの先見性の欠如が原因です。 これらの状況を可能な限り予測することが重要であり、ユニットテストが適切な場所であることがよくあります。 Person
とPersonTest
を構築する際に、予想される例外のテストをいくつか含めましたが、それは決して完全ではありませんでした。 たとえば、タイムゾーンデータを表したり保存したりしないLocalDate
を使用します。 ただし、 LocalDate.now()
を呼び出すと、システムのデフォルトのタイムゾーンに基づいてLocalDate
が返されます。これは、システムのユーザーのタイムゾーンよりも1日早いまたは遅い可能性があります。 これらの要因は、適切なテストと動作を実装して検討する必要があります。
境界もテストする必要があります。 getDaysUntilBirthday()
メソッドを持つPerson
オブジェクトについて考えてみます。 テストには、その人の誕生日が今年にすでに過ぎているかどうか、その人の誕生日が今日であるかどうか、うるう年が日数にどのように影響するかを含める必要があります。 これらのシナリオは、その人の誕生日の前日、その人の誕生日の翌日、翌年がうるう年である日を確認することでカバーできます。 関連するテストコードは次のとおりです。
PersonTest.java
// ... class PersonTest { private final Supplier<LocalDate> currentDateSupplier = ()-> LocalDate.parse("2015-05-02"); private final LocalDate ageJustOver5 = LocalDate.parse("2010-05-01"); private final LocalDate ageExactly5 = LocalDate.parse("2010-05-02"); private final LocalDate ageAlmost5 = LocalDate.parse("2010-05-03"); // ... @Test void testGetDaysUntilBirthday() { assertAll( createPersonAndAssertValue(ageAlmost5, 1, Person::getDaysUntilBirthday), createPersonAndAssertValue(ageExactly5, 0, Person::getDaysUntilBirthday), createPersonAndAssertValue(ageJustOver5, 365, Person::getDaysUntilBirthday) ); } private Executable createPersonAndAssertValue(LocalDate dateOfBirth, long expectedValue, Function<Person, Long> personLongFunction) { Person person = new Person("Given", "Sur", dateOfBirth, currentDateSupplier); long actualValue = personLongFunction.apply(person); return () -> assertEquals(expectedValue, actualValue); } }
統合テスト
私たちは主に単体テストに焦点を当ててきましたが、JUnitは統合、受け入れ、機能、およびシステムテストにも使用できます。 このようなテストでは、サーバーの起動、既知のデータを含むデータベースのロードなど、より多くのセットアップコードが必要になることがよくあります。多くの場合、数千の単体テストを数秒で実行できますが、大規模な統合テストスイートの実行には数分または数時間かかる場合があります。 統合テストは、通常、コード内のすべての順列またはパスをカバーしようとするために使用されるべきではありません。 そのためには、単体テストの方が適しています。
フォームへの入力、ボタンのクリック、コンテンツのロードの待機などでWebブラウザーを駆動するWebアプリケーションのテストの作成は、通常、「ページオブジェクトパターン」と組み合わせたSelenium WebDriver(Apache 2.0ライセンス)を使用して行われます(SeleniumHQ github wikiを参照)。およびMartinFowlerのページオブジェクトに関する記事)。
JUnitは、ApacheHTTPクライアントやSpringRestTemplateなどのHTTPクライアントを使用してRESTfulAPIをテストするのに効果的です(HowToDoInJava.comが良い例です)。
Person
オブジェクトの場合、統合テストでは、People BirthdaysサービスのベースURLを指定する構成で、モックではなく実際のBirthdaysClient
を使用することができます。 次に、統合テストはそのようなサービスのテストインスタンスを使用し、誕生日がそれに公開されていることを確認し、返されるサービスで有名人を作成します。
その他のJUnit機能
JUnitには、例ではまだ検討していない多くの追加機能があります。 いくつかを説明し、他のリファレンスを提供します。
テストフィクスチャ
JUnitは、各@Test
メソッドを実行するためのテストクラスの新しいインスタンスを作成することに注意してください。 JUnitは、すべてまたは各@Test
メソッドの前または後に特定のメソッドを実行するためのアノテーションフックも提供します。 これらのフックは、データベースまたはモックオブジェクトのセットアップまたはクリーンアップによく使用され、JUnit4と5では異なります。
JUnit 4 | JUnit 5 | 静的メソッドの場合? |
---|---|---|
@BeforeClass | @BeforeAll | はい |
@AfterClass | @AfterAll | はい |
@Before | @BeforeEach | 番号 |
@After | @AfterEach | 番号 |
PersonTest
の例では、@ @Test
メソッド自体でBirthdaysClient
モックオブジェクトを構成することを選択しましたが、複数のオブジェクトを含むより複雑なモック構造を構築する必要がある場合があります。 @BeforeEach
(JUnit 5の場合)および@Before
(JUnit 4の場合)がこれに適していることがよくあります。
@After*
アノテーションは、JVMガベージコレクションが単体テスト用に作成されたほとんどのオブジェクトを処理するため、単体テストよりも統合テストで一般的です。 @BeforeClass
および@BeforeAll
アノテーションは、各テストメソッドではなく、コストのかかるセットアップおよびティアダウンアクションを1回実行する必要がある統合テストに最も一般的に使用されます。
JUnit 4については、テストフィクスチャガイドを参照してください(一般的な概念はJUnit 5にも適用されます)。
テストスイート
複数の関連するテストを実行したい場合がありますが、すべてのテストを実行するわけではありません。 この場合、テストのグループをテストスイートに構成できます。 JUnit 5でこれを行う方法については、HowToProgram.xyzのJUnit 5の記事、およびJUnitチームのJUnit4のドキュメントを確認してください。
JUnit5の@Nestedおよび@DisplayName
JUnit 5には、静的ではないネストされた内部クラスを使用して、テスト間の関係をより適切に示す機能が追加されています。 これは、JasmineforJavaScriptのようなテストフレームワークでネストされた記述を使用したことがある人には非常によく知られているはずです。 これを使用するために、内部クラスには@Nested
アノテーションが付けられています。
@DisplayName
アノテーションもJUnit5の新機能であり、テストメソッド識別子に加えて表示される文字列形式でレポート用のテストを記述できます。
@Nested
と@DisplayName
は互いに独立して使用できますが、一緒に使用すると、システムの動作を説明するより明確なテスト結果を提供できます。
ハムクレストマッチャー
Hamcrestフレームワークは、それ自体はJUnitコードベースの一部ではありませんが、テストで従来のassertメソッドを使用する代わりの方法を提供し、より表現力豊かで読みやすいテストコードを可能にします。 従来のassertEqualsとHamcrestassertThatの両方を使用した次の検証を参照してください。
//Traditional assert assertEquals("Hayden, Josh", displayName); //Hamcrest assert assertThat(displayName, equalTo("Hayden, Josh"));
Hamcrestは、JUnit 4と5の両方で使用できます。Hamcrestに関するVogella.comのチュートリアルは、非常に包括的です。
追加リソース
記事「単体テスト」、「テスト可能なコードの書き方」、および「なぜそれが重要なのか」では、クリーンでテスト可能なコードを書くためのより具体的な例について説明しています。
自信を持って構築する:JUnitテストのガイドでは、ユニットテストと統合テストのさまざまなアプローチと、1つを選択してそれを維持することが最善である理由について説明します。
JUnit 4WikiおよびJUnit5ユーザーガイドは、常に優れたリファレンスポイントです。
Mockitoのドキュメントには、追加の機能と例に関する情報が記載されています。
JUnitは自動化への道です
JUnitを使用したJavaの世界でのテストの多くの側面を調査してきました。 Javaコードベース用のJUnitフレームワークを使用したユニットテストと統合テスト、開発環境とビルド環境でのJUnitの統合、サプライヤーとMockitoでのモックとスタブの使用方法、一般的な規則とベストコードプラクティス、テスト対象、およびいくつかのテストについて見てきました。その他の優れたJUnit機能。
今度は、JUnitフレームワークを使用した自動テストの利点を巧みに適用、維持、および享受することに成長する読者の番です。