JUnitとは、Javaのクラスやメソッドが期待どおりに動くかを、テストコードとして書いて自動で確かめるための単体テストフレームワークです。読み方は「ジェイユニット」です。この記事では、割引計算のクラスを題材に、JUnit 6.1.3・Mockito 5.24.0・Maven Surefire 3.6.1・JDK 25の組み合わせで実際に動かしたコードと出力を使って、導入からテストの書き方、失敗したときの出力の読み方、JUnit 6で変わった点までを順に説明します(版は2026年10月27日時点のMaven Centralの最新です)。
まとめ:JUnitの要点
- 何をするものか:Javaのコードを小さな単位で検証するテストを書き、何度でも同じ条件で実行できるようにする。失敗すると「期待値」と「実際の値」を並べて表示する。
- 最新版はJUnit 6.1.3(2026年8月7日)。JUnit 6はテストを実行するJVMにJava 17以上が必須。Java 8〜16でしか実行できない環境はJUnit 5.14系を使う。JUnit 4は保守モードで、新規に選ぶ理由はない。
- 書き方の核:
@Testを付けたメソッドの中でassertEqualsなどのアサーションを呼ぶ。入力を並べたいときは@ParameterizedTest、外部依存はMockitoで差し替える。 - 導入でつまずく所:MavenはSurefire 3.0.0以上が必要。JDK 21以降でMockitoを使うと警告が出るので、
-javaagentの設定を足す。
以下、定義と版の違いを押さえたあと、導入・書き方・実行・モックの順に進みます。
JUnitの役割と成り立ち
単体テストとは、クラスやメソッドといった最小単位が単独で正しく動くかを確かめるテストです。JUnitでは、確かめたい内容をJavaのコードとして書き、ビルドツールやIDEから一括で実行します。mainメソッドを書き換えて手で動かす確認と違い、テストはリポジトリに残り、コードを直すたびに同じ検証を繰り返せます。修正で以前の機能が壊れていないか(回帰)を、毎回機械に確かめさせられる点が一番の価値です。
xUnitの系譜とJUnit 3・4・5・6の流れ
JUnitはErich GammaとKent Beckが書いたもので、単体テストフレームワークの「xUnit」アーキテクチャの一実装です(JUnit FAQ)。JUnit 3ではテストクラスが TestCase を継承し、メソッド名を test で始める決まりでした。JUnit 4で @Test などのアノテーションに移り、JUnit 5で内部を3つのモジュールに分けました。JUnit 6はこの5の構成を引き継いだまま、最低Javaバージョンを17へ上げた版です。
Platform・Jupiter・Vintageの分担
JUnit 5以降のJUnitは3つの部品の組み合わせです(JUnit User Guide)。
- JUnit Platform:JVM上でテストフレームワークを起動する土台。MavenやGradle、IDEはここを通してテストを動かす。
- JUnit Jupiter:テストを書くためのAPI(
@Testやアサーション)と拡張の仕組み。普段「JUnitで書く」と言うときに使うのはここ。 - JUnit Vintage:JUnit 3・4で書かれた古いテストをPlatform上で動かすためのエンジン。
依存に junit-jupiter と書くとJupiterとPlatformの必要な部品がまとめて入ります。Vintageは、JUnit 4のテストが残っているプロジェクトだけが追加します。
JUnit 6の変更点と、JUnit 5・4の現状
2026年10月27日時点の各系統の状況です。日付は公式のリリースノートとGitHubの記載に基づきます。
| 系統 | 最新版 | 実行に必要なJava | 状態 |
|---|---|---|---|
| JUnit 6 | 6.1.3(2026-08-07) | 17 | 開発の本線 |
| JUnit 5 | 5.14.4(2026-04-26) | 8 | 修正のみ継続 |
| JUnit 4 | 4.13.2(2021-02) | 5 | 保守モード |
JUnit 6.0で変わったこと
JUnit 6.0.0は2025年9月30日に公開されました(6.0.0リリースノート)。利用者に影響が大きいのは次の点です。
- JUnitを動かすJVMにJava 17以上が必須になった。古いJavaバージョン向けにコンパイルしたコードもテストはできるが、テストを実行するJDKを17未満から上げられないプロジェクトは6を使えない。
- Platform・Jupiter・Vintageのバージョン番号が1つに揃った。JUnit 5ではPlatformだけ1.x系という分かれ方をしていたが、6では全部が6.x になる。
- 全モジュールにJSpecifyのnull許容アノテーションが付いた。KotlinではテストメソッドとライフサイクルメソッドにKotlinの
suspendを付けられる。 @CsvSourceと@CsvFileSourceのCSV解析ライブラリがunivocity-parsersからFastCSVに替わった。- 非推奨だった
MethodOrderer.AlphanumericなどのAPIが削除された。 - Vintageエンジンが非推奨になった。JUnit 4のテストクラスを見つけるとINFOレベルの通知を出す。
Mavenで動かす場合はSurefire/Failsafeが3.0.0以上であることも条件です(Build Support)。古いpom.xmlでSurefireの版を固定していると、JUnit 6に上げた時点でテストが動かなくなります。
JUnit 6.1で足された機能
6.1.0は2026年5月19日の公開です(6.1.0リリースノート)。テストの書き方に関わるのは、既定のロケールとタイムゾーンをテスト中だけ切り替える @DefaultLocale・@DefaultTimeZone と、システムプロパティを設定して終了後に元へ戻す組み込み拡張です。これまでJUnit Pioneerなどの外部ライブラリに頼っていた用途が、本体だけで書けるようになりました。ほかに、@TempDir の一時ディレクトリを削除する方式と、削除できないファイルがあったときの扱いの指定、compact source filesでテストを書いて実行するための org.junit.start モジュール、通常のスレッドプールを使う並列実行の実装が加わっています。
JUnit 5・4を使い続ける条件
JUnit 6の公開後も5系の修正版は出ており、5.14.4は2026年4月26日の公開です。メンテナーは5系について「大きな新機能は入れず、重要な修正はバックポートする」と述べています(GitHub Discussions #5054)。テストを実行するJDKをJava 17へ上げられない業務システムでは、5.14系を使うのが正しい選択です。
JUnit 4は「maintenance mode」と明記され、直すのは重大なバグとセキュリティ問題だけです(junit4リポジトリ)。新しく書くテストに4を選ぶ理由はありません。既存の4のテストは、Vintageエンジンで動かしたまま、触るたびにJupiterへ書き換えていく進め方が現実的です。ただし6ではVintage自体が非推奨なので、移行を先送りし続けることはできません。
Maven・GradleへのJUnit導入手順
JUnitは通常、ビルドツールの依存として追加します(ビルドツールなしでコマンドラインから動かすための junit-platform-console-standalone のjarも公式に配布されていますが、この記事ではMavenとGradleで説明します)。バージョンの食い違いを避けるため、公式はBOM(junit-bom)で版を揃える書き方を勧めています。Spring Bootを使う場合は、spring-boot-starter-test がJUnit Jupiterを含み、版もSpring Boot側で管理されるので、ここでの設定は要りません。
Mavenの場合は、pom.xmlに次の設定を加えます。JUnitに関わる部分の抜粋で、今回の検証ではこれに加えて maven.compiler.release を17に設定し、後述のMockito依存を追加しました。
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>6.1.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.6.1</version>
</plugin>
</plugins>
</build>
Gradle(Kotlin DSL)の場合です。java プラグインと mavenCentral() のリポジトリ設定が済んでいる build.gradle.kts に追加する例です。JUnitの公式ドキュメントの例でも、junit-platform-launcher を testRuntimeOnly で明示しています。
dependencies {
testImplementation(platform("org.junit:junit-bom:6.1.3"))
testImplementation("org.junit.jupiter:junit-jupiter")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
tasks.test {
useJUnitPlatform()
}
テストクラスは本番コードと同じパッケージ構成で src/test/java の下に置き、対象が PriceCalculator なら PriceCalculatorTest と名付けるのが慣習です。Surefireは既定で *Test・Test*・*Tests・*TestCase という名前のクラスを拾うので、この命名から外れるとテストが実行されません。
割引計算を題材にしたJUnitテストの書き方
ここからは、10個以上買うと1割引になり、数量が1未満なら例外を投げる計算クラスをテストします。本番コードは src/main/java/shop/PriceCalculator.java に置きます。
package shop;
public class PriceCalculator {
// 10個以上で1割引。数量が1未満なら例外
public int total(int unitPrice, int quantity) {
if (quantity < 1) {
throw new IllegalArgumentException("quantity must be >= 1");
}
int subtotal = unitPrice * quantity;
return quantity >= 10 ? subtotal * 90 / 100 : subtotal;
}
}
@Testとアサーションによる期待値の検証
テストクラスは src/test/java/shop/PriceCalculatorTest.java に作ります。この後のパラメータ化テストと @Nested の断片もこのクラスの中に追加するので、必要なimportは最初にまとめて書いてあります。テストメソッドには @Test を付け、中で「期待する値」と「実際の値」をアサーションで比べます。assertEquals は第1引数が期待値、第2引数が実際の値です。逆に書いても成否は同じですが、失敗時のメッセージで期待値と実際の値が入れ替わって表示され、原因の読み違いにつながります。
package shop;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
class PriceCalculatorTest {
private PriceCalculator calc;
@BeforeEach
void setUp() {
calc = new PriceCalculator(); // テストごとに新しいインスタンス
}
@Test
@DisplayName("9個までは定価の合計")
void noDiscountUnderTen() {
assertEquals(900, calc.total(100, 9));
}
@Test
void groupedAssertions() {
assertAll(
() -> assertEquals(100, calc.total(100, 1)),
() -> assertEquals(900, calc.total(100, 10))
);
}
}
よく使うアサーションは次のとおりです。
assertEquals(expected, actual):値が等しいかassertTrue(cond)/assertFalse(cond):条件が真か偽か。assertFalse(list.isEmpty())のように「そうでないこと」を確かめるときはassertFalseを使うassertNull/assertNotNull:nullかどうかassertThrows(型, 処理):例外が投げられるか。戻り値の例外からメッセージも検証できるassertAll(...):複数の検証をまとめて実行し、途中で失敗しても残りを最後まで確かめる
assertThat はJUnit Jupiterの Assertions にはありません。assertThat(actual).isEqualTo(expected) の形で書きたい場合は、AssertJ(org.assertj:assertj-core)を別に追加します。
@ParameterizedTestによる入力と期待値の一覧化
境界値(9個と10個)のように入力だけを変えて同じ検証をするなら、テストメソッドを複製せず @ParameterizedTest と @CsvSource で表にします。依存の junit-jupiter にはパラメータ化テストのモジュールも含まれています。
@ParameterizedTest(name = "{0}円×{1}個 = {2}円")
@CsvSource({
"100, 1, 100",
"100, 9, 900",
"100, 10, 900",
"250, 12, 2700"
})
void totals(int unitPrice, int quantity, int expected) {
assertEquals(expected, calc.total(unitPrice, quantity));
}
この1メソッドは4件のテストとして数えられます。name に書いた書式はIDEの結果表示に使われ、どの行が失敗したかが「100円×10個 = 900円」の形で分かります。
@Nestedと@DisplayNameによるテスト条件の整理
正常系と異常系が混ざって読みにくくなったら、内部クラスに @Nested を付けて条件ごとに分けます。外側の @BeforeEach は内側のテストでも実行されるので、calc をそのまま使えます。
@Nested
@DisplayName("不正な数量")
class InvalidQuantity {
@Test
void zeroThrows() {
IllegalArgumentException e = assertThrows(
IllegalArgumentException.class, () -> calc.total(100, 0));
assertEquals("quantity must be >= 1", e.getMessage());
}
}
@DisplayName の日本語名はIDEの結果ツリーに出ます。一方、Surefireが target/surefire-reports に書き出すXMLでは、今回の検証でも noDiscountUnderTen のようにメソッド名のまま記録されました。CIのレポートで読むことも考えるなら、メソッド名自体も意味の通る名前にしておきます。
JUnitテストの実行方法と成功・失敗ログの読み方
IDE(IntelliJ IDEAやEclipse)ではテストクラスやメソッドの横の実行ボタンから動かせます。コマンドラインでは次のように実行します。
mvn test # 全テスト
mvn test -Dtest=PriceCalculatorTest # クラスを指定
gradle test --tests "shop.PriceCalculatorTest"
全テストが通ると、Mavenは次のように出力します。Tests run: 7 は、通常の @Test 3件と、パラメータ化テストの4件を合わせた数です。
[INFO] Tests run: 7, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.503 s -- in shop.PriceCalculatorTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.258 s -- in shop.CheckoutServiceTest
[INFO] Tests run: 8, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
失敗したときの出力も見ておきます。10個の合計を誤って1000円と期待したテストと、assertAll で2つの検証をまとめたテストをわざと落とした結果です。
[ERROR] shop.FailDemoTest.wrongExpectation -- Time elapsed: 0.104 s <<< FAILURE!
org.opentest4j.AssertionFailedError: expected: <1000> but was: <900>
at shop.FailDemoTest.wrongExpectation(FailDemoTest.java:9)
[ERROR] shop.FailDemoTest.groupedFailures -- Time elapsed: 0.042 s <<< FAILURE!
org.opentest4j.MultipleFailuresError:
Multiple Failures (2 failures)
org.opentest4j.AssertionFailedError: expected: <1000> but was: <900>
org.opentest4j.AssertionFailedError: expected: <250> but was: <200>
読む順番は決まっています。まず expected: <1000> but was: <900> で期待値と実際の値を比べ、次に FailDemoTest.java:9 の行番号で失敗したアサーションを開きます。この例では計算ではなく期待値の側が誤っています(10個から1割引なので900円が正しい)。assertAll では Multiple Failures (2 failures) と失敗が全件並ぶため、1件目を直して再実行し、次の失敗を見る往復が要りません。
出力の Failures はアサーションが満たされなかったもの、Errors はテスト中に想定外の例外が出たものです。Errors が増えたときはテスト対象かテストの準備コードが落ちているので、スタックトレースの一番上にある自分のコードの行から見ます。値の変化を追いたいときは、IDEで失敗したテストメソッドだけをデバッグ実行し、ブレークポイントで止めます。
Mockitoによる外部依存の差し替え
データベースや外部APIを呼ぶクラスは、そのままでは単体テストが遅く、結果も不安定になります。依存をインターフェースで受け取る形にしておけば、テストではMockitoが作る偽物(モック)を渡せます。次の CheckoutService は、クーポンの割引率を CouponRepository インターフェースから取得します(どちらも shop パッケージに置きます)。
package shop;
import java.util.Optional;
public interface CouponRepository {
Optional<Integer> findRate(String code);
}
package shop;
public class CheckoutService {
private final CouponRepository coupons;
public CheckoutService(CouponRepository coupons) {
this.coupons = coupons;
}
public int pay(int amount, String couponCode) {
int rate = coupons.findRate(couponCode).orElse(0);
return amount * (100 - rate) / 100;
}
}
テストでは mockito-junit-jupiter を依存に足します。JUnitのBOMはMockitoの版を管理しないので、版は明示します。
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.24.0</version>
<scope>test</scope>
</dependency>
テストクラス src/test/java/shop/CheckoutServiceTest.java では、MockitoExtension でモックを注入します。when(...).thenReturn(...) で返す値を決め、verify で呼ばれたことを確かめます。
package shop;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.*;
import java.util.Optional;
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)
class CheckoutServiceTest {
@Mock
CouponRepository coupons;
@InjectMocks
CheckoutService service;
@Test
void appliesCouponRate() {
when(coupons.findRate("SPRING20")).thenReturn(Optional.of(20));
assertEquals(800, service.pay(1000, "SPRING20"));
verify(coupons).findRate("SPRING20");
}
}
JDK 21以降で出るMockitoの警告と対処
このテストをJDK 25で実行すると、テストは通るものの次の警告が出ました。
Mockito is currently self-attaching to enable the inline-mock-maker. This will no longer work in future releases of the JDK. Please add Mockito as an agent to your build as described in Mockito's documentation
WARNING: A Java agent has been loaded dynamically (.../byte-buddy-agent-1.17.7.jar)
Java 21からJDKはライブラリが実行中のJVMへ自分でエージェントを読み込むことを制限しており、Mockitoの公式ドキュメントは、mockito-coreのjarを -javaagent で明示的に指定するよう案内しています(Mockito 0.3節)。Mavenでは、dependencyプラグインの properties ゴールでjarのパスをプロパティにし、Surefireの argLine に渡します。
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>properties</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.6.1</version>
<configuration>
<argLine>-javaagent:${org.mockito:mockito-core:jar}</argLine>
</configuration>
</plugin>
この設定で再実行すると警告は消え、8件とも成功しました。なお、検証では最初に <argLine>@{argLine} -javaagent:...</argLine> と書いたところ、@{argLine} が置き換えられずにそのままJVMへ渡り、The forked VM terminated without properly saying goodbye でテストが1件も動きませんでした。@{argLine} はJaCoCoなど別のプラグインが argLine プロパティを設定しているときに使う書き方で、何も設定していないプロジェクトでは付けないでください。モックとスタブの違いなど、テストダブルの考え方はモックとは?スタブとの違い・テストダブル5分類と5言語の書き方を実行例で解説で扱っています。staticメソッドのモックが必要な古いコードについてはPowerMockitoとは?使い方と@PrepareForTestの指定ルール、Mockito 5.24へ移行する判断を参照してください。
テストを書く場所・書かない場所
JUnitを入れた直後は、カバレッジ(テストの実行中に通過したコードの割合)を100%にしたくなります。しかし、IDEやLombokが生成した、処理を持たないgetter・setterにテストを書いても、検証できるのはフィールドへの代入と読み出しだけで、保守するテストの数が増えます。手書きの詰め替え処理は、項目の取り違えや変換の誤りが起きやすいものから順に検証します。優先して書くのは次の箇所です。
- 分岐や計算があるロジック(今回の割引の境界のように、条件が変わる値の前後)
- 過去にバグが出た箇所。直したら、そのバグを再現するテストを先に足す
- これからリファクタリングする箇所。書き換える前に今の振る舞いをテストで固定する
避けたいのは、モックを多用して「メソッドAがメソッドBを呼んだこと」ばかり検証するテストです。実装の手順に結び付いたテストは、振る舞いを変えないリファクタリングでも壊れ、やがて誰も直さなくなります。verify は、外部へのメール送信のように呼んだこと自体が仕様である場合に絞ります。AIでテストを生成した場合も、期待値が実装の出力をそのまま写していないかは人が確かめます。実装が間違っていれば、テストも同じ間違いを正解として固定してしまうためです。
カバレッジの指標の読み方はテストカバレッジとは?C0/C1/C2の網羅率と計測ツール・目標設定を実装者向けに解説、単体テストと結合テスト・E2Eの配分はテストピラミッドとは?単体・結合・E2Eの配分と実装者向けテスト戦略設計が詳しく扱っています。JUnitのテストをCIで毎回回す仕組みづくりから相談したい場合は、DevOps・CI/CD導入支援でも対応しています。
よくある質問
JUnitとは一言でいうと何ですか?
Javaのコードが期待どおりに動くかを、テストコードで自動検証するための単体テストフレームワークです。読み方は「ジェイユニット」です。
JUnit 4・5・6のどれを使えばよいですか?
Java 17以上ならJUnit 6(2026年10月時点で6.1.3)、Java 8〜16ならJUnit 5.14系です。JUnit 4は保守モードのため新規には選ばず、既存のテストはVintageエンジンで動かしながらJupiterへ移します。
テストクラスとは何ですか?どこに置きますか?
テストメソッドをまとめたクラスで、テスト対象のクラスごとに1つ作るのが基本です。src/test/java の下に本番コードと同じパッケージで置き、PriceCalculatorTest のように名前の末尾を Test にします。Mavenはこの命名のクラスを自動で実行します。
assertTrueとassertFalseはどう使い分けますか?
条件が真であることを確かめるのが assertTrue、偽であることを確かめるのが assertFalse です。assertTrue(!x) と否定を重ねるより、assertFalse(x) と書く方が失敗時に意図が読み取りやすくなります。値の比較には assertEquals を使います。
JUnitでテストが失敗したときはどこから見ればよいですか?
まず expected: <…> but was: <…> の2つの値を比べ、期待値と実装のどちらが誤っているかを判断します。次にスタックトレースのテストクラスの行番号を開きます。原因が分からなければ、IDEでそのテストメソッドだけをデバッグ実行してください。