JUnit5 基礎学習
背景
JavaのユニットテストフレームワークであるJUnitを個人で学習した内容を記載します。
初学者向けへの発信、個人の備忘録が主な用途です。
- 対象者
- Javaについてある程度理解ができている方
- ユニットテストを実施したことがない方
- テストコードを作成してテストしたことがない方
- ホワイトボックステストのやり方を知りたい方
- 実務で利用するため学習が必要な方
動作環境
- Windows 11
- Java 11
- eclipse
- Gradle
- JUnit 5
- Mockito
ユニットテスト(UT)とは?
機能を構成する部品の最小単位で行うテストのことを指します。
最小単位がプロジェクト/案件によって異なる場合がありますが、基本的にはクラス単位となります。(1機能単位で最小単位とするケースもある。)
よくある開発モデルでV字と呼ばれるものがありますが、詳細/内部設計書の内容が正しいかどうかを確認します。また設計不良も発見することも目的としています。
ホワイトボックステスト
ユニットテスト(以下UT)では、主にホワイトボックステストというテスト技法が用いられます。
ホワイトボックステストは、プログラムの内部構造が意図した動作になっているかを確認します。カバレッジと呼ばれる、処理の網羅性を計測して、コーディングしたプログラムが動いているかをはっきり確認できるテストになっています。
それとは対になるのが、ブラックボックステストと呼ばれており、プログラムの内部構造を意識せずに外部仕様のみ(入出力のみ)を確認してテストする方法となります。画面や帳票のテストや結合テスト以降で利用されます。
カバレッジ
UTの指標です。カバレッジの種類は結構多いですが、主に基準となるカバレッジは下記になります。
- C0:命令網羅(ステートメント・カバレッジ)
- C1:分岐網羅(ブランチ・カバレッジ)
- C2:条件網羅(コンディション・カバレッジ)
上記をカバレッジ100%になるようにテストを行っていきます。ただし、処理到達が不可能なコードがあったり、決められた網羅率を超えている場合は、テスト完了とする場合があります。
※カバレッジを100%にしてもバグが全て回収できるわけではないので、注意。ただし、単純なバグは抑制できると思います。プロジェクトによって、網羅率はまちまちなのでそちらに従うこと。
JUnitの基礎
JUnitはテスト実行と実行結果の確認を自動化してくれるツールです。
自動化を行うためには、テストケースを考えて、テストコード(テストスクリプトとも呼ばれます)を作成する必要があります。
jUnitとMockitoのライブラリ追加
外部のライブラリとして追加する必要があるので、その手順を記載。
基本的には、Gradleでプロジェクトを作成すれば、デフォルトで入っていると思います。もし存在しなければ、下記のファイルのdependenciesに追加してGradleプロジェクトを更新する必要があります。(Mavenの場合は、pom.xmlのdependenciesへ追加することになります)
Mockitoも同様に追加が必要です。
- build.gradle
dependencies {
testImplementation "org.junit.jupiter:junit-jupiter:5.7.0"
testImplementation "org.mockito:mockito-core:3.7.7"
testImplementation "org.mockito:mockito-inline:3.7.7"
testImplementation "org.mockito:mockito-junit-jupiter:3.7.7"
testImplementation "org.junit.platform:junit-platform-launcher:1.7.0"
}
テストクラスの作成
テストクラスを作成するフォルダは、srcではなく、testフォルダになります。
その中にsrcと同じパッケージを作成してテストクラスを作成します。IDEを用いると自動作成できます。eclipseの場合は、テスト対象クラスを開いて、「Ctrl + 9」をクリックすることで自動作成可能です。
Intelij IDEAでは、「Alt + Enter」で作成できるようです。
テストクラスは、テスト対象クラス名+Testという名前で作成します。
- テスト対象クラス:Calculator
- テストクラス;CalculatorTest
テストメソッドの作成
テストメソッドにはアノテーション「@Test」をつける必要がある。
- Calculator.java
package junit.sample;
import java.math.BigDecimal;
import java.math.RoundingMode;
/**
* 足し算引き算処理クラス
* @author
*/
public class Calculator {
private final BigDecimal ONE_HUNDRED = new BigDecimal(100);
public int add(int x, int y) {
return x + y;
}
public int sub(int x, int y) {
return x - y;
}
}
- CalculatorTest.java
package org.example;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
public class CalculatorTest {
public Calculator createCalculator() {
return new Calculator();
}
@Test
@DisplayName("addメソッド正常系")
public void testAdd() {
var cal = createCalculator();
int actual = cal.add(2, 3);
assertEquals(5, actual);
}
@Test
@DisplayName("subメソッド正常系")
public void testSub() {
var cal = createCalculator();
int actual = cal.su(10, 3);
assertEquals(7, actual);
}
}
JUnit標準のアノテーション一覧
下記公式ドキュメント
代表的なアサーション一覧
下記Webページにわかりやすくまとめられているため、引用
パラメータ化テスト
アノテーション「@ParameterizedTest」をテストメソッドに付与します。
パラメータの形式は下記に記載されております。
データを自分で作成するなら、@ValueSourceか@CsvSourceになりますが、CSVでは複数の引数を渡せるので、こちらのほうが使い勝手が良い気がします。
ライフサイクル
アノテーションの実行タイミングについて、記載します。
- @BeforeAll
- テスト実行の一番最初に1度だけ実行したい処理に付与する。
- 初期化処理などで利用する。
- @BeforeEach
- テストメソッドごとに最初に実行する処理に付与する。
- BeforeAllとの違いは、1度だけ実行するか、テストメソッドごとに実行できるか
- @Nestedで入れ子になっているメソッドには、新しく付与できる
- @AfterAll
- テスト実行の一番最後に1度だけ実行したい処理に付与する。
- 終了処理などで利用する。
- @AfterEach
- テストメソッドごとに最後に実行する処理に付与する。
- AfterAllとの違いは、1度だけ実行するか、テストメソッドごとに実行できるか
- @Nestedで入れ子になっているメソッドには、新しく付与できる
下記のようなコードで起債している場合のライフサイクルについて記載する。
// 最初に実行
@BeforeAll
public static void beforeAll() {
System.out.println("BeforeAll実行されました");
}
// 各テストメソッドの最初に実行
@BeforeEach
public void beforeEach() {
System.out.println("BeforeEach実行されました");
}
// 最初に実行されるテストメソッド
@Test
public void testA() {
System.out.println("テストAが実行されました");
}
// 2個目に実行されるテストメソッド
@Test
public void testB() {
System.out.println("テストB実行が実行されました");
}
// 3個目に実行されるテストメソッド
@Test
public void testC() {
System.out.println("テストCが実行されました");
}
// 各テストメソッドの最後に実行
@AfterEach
public void afterEach() {
System.out.println("AfterEachが実行されました");
}
@Nested
public class Nest1 {
// ネストの中の各メソッドで最初に実行
@BeforeEach
public void nestBeforeEach() {
System.out.println("[ Nest1 ]BeforeEachが実行されました");
}
// ネストの中で最初に実行されるテストメソッド
@Test
public void test1() {
System.out.println("[ Nest1 ] テスト1が実行されました");
}
// ネストの中で2個目に実行されるテストメソッド
@Test
public void test2() {
System.out.println("[ Nest1 ] テスト2が実行されました");
}
// ネストの中で3個目に実行されるテストメソッド
@AfterEach
public void nestAfterEach() {
System.out.println("[ Nest1 ]AfterEachが実行されました");
}
}
// 一番最後に実行
@AfterAll
public static void afterAll() {
System.out.println("AfterAllが実行されました");
}
データベーステスト
DBUnitを用いて、JDBCの処理をテストできます。
Spring bootでDBUnitを利用する場合、接続情報などの初期設定が不要となるようなので、
Spring bootのUTを記事にするときに改めて記載します。
Mockitoによるモック&スタブを用いたテスト
JUnitでモックを簡易に作成するためのフレームワークです。
他のクラスに依存している場合は、モック化してテストを実施しなければいけない場合があります。
特に下記のような場合にモック化を行います。
- 依存するクラスの実装が未完了の場合
- 依存するクラスの対象メソッドの処理結果が不確定である場合(ランダム値など)
依存クラスのモック化
テストクラスに下記のアノテーションをつける必要があります。
- @ExtendWith(MockitoExtension.class)
モック化したいクラスのインスタンスには、下記のアノテーションをつけます。
- @Mock
モック化したクラスを注入するため、テスト対象クラスのインスタンスに下記のアノテーションをつけます。
- @InjectMocks
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
public class SampleServiceTest {
@Mock
private SampleService sampleService;
@InjectMocks
private TaxAmountService taxAmountService;
@Test
void test1() {
// スタブ作成
when(sampleService.getData(any())).thenReturn(10);
int actual = sampleService.getData("1")
assertEquals(10, actual);
}
}
依存クラスの一部メソッドをモック化
上記は依存クラスを全体をモック化しましたが、依存クラス事態は作成されており、一部のメソッド以外は、実メソッドでテストを行いないという場合があるかと思います。
その場合は、下記のSpyアノテーションを利用します。
- @Spy
public class SampleService {
public int getData(int key) {
// モック実装
return 0;
}
public int dataAdd(int i, int j) {
// 実装済み
return i + j;
}
}
@ExtendWith(MockitoExtension.class)
public class SampleServiceTest {
// 一部をモック化
@Spy
private SampleService sampleService;
@InjectMocks
private TaxAmountService taxAmountService;
@Test
void testGetData() {
// スタブ作成
when(sampleService.getData(any())).thenReturn(10);
int actual = sampleService.getData("1")
assertEquals(10, actual);
}
@Test
void testDataAdd() {
// 実メソッドの処理を利用
int actual = samoleService.DataAdd(10, 5);
assertEquals(15, actual)
}
}
Discussion