- 技術 💻
JUnitの使い方|単体テストの書き方と観点【JUnit 5・6】
JUnitは、Javaでプログラムの部品(メソッドやクラス)が正しく動くかを自動で確かめる「単体テスト」を書くための、定番のフレームワークです。@Test を付けたメソッドの中で、処理を実行し、結果が期待どおりかを assertEquals などで確認します。
現在の主流はJUnit 5(Jupiter)で、2025年にはJUnit 6も公開されました。JUnit 6はJava 17以上が必要になるなどの変更がありますが、テストの書き方の基本はJUnit 5と共通です。この記事では、環境の設定から、よく使う機能、Mockitoを使った依存の差し替え、そして何をテストすべきかという観点までを解説します。
環境の設定
Maven
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>5.11.4</version> <!-- JUnit 6 を使う場合は 6.x(Java 17以上) -->
<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>
テストは mvn test で実行します。Maven Surefire Pluginは、比較的新しいバージョン(3.x)を使います。
Gradle
dependencies {
testImplementation platform("org.junit:junit-bom:5.11.4")
testImplementation "org.junit.jupiter:junit-jupiter"
testRuntimeOnly "org.junit.platform:junit-platform-launcher"
}
test {
useJUnitPlatform() // これがないとテストが実行されない
}
テストは gradle test(または ./gradlew test)で実行します。
Spring Bootのプロジェクトでは、spring-boot-starter-test にJUnit・Mockito・AssertJが含まれているため、追加の設定は不要です。
ファイルの置き場所
src/main/java/com/example/PriceCalculator.java ← テストされるクラス
src/test/java/com/example/PriceCalculatorTest.java ← テストクラス(同じパッケージ)
最初のテスト
テストされるクラスです。
public class PriceCalculator {
/** 税抜価格から税込価格を計算する(1円未満切り捨て) */
public int withTax(int price, int ratePercent) {
if (price < 0) {
throw new IllegalArgumentException("価格は0以上");
}
return price * (100 + ratePercent) / 100;
}
}
テストクラスです。
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
class PriceCalculatorTest {
@Test
void 税率10パーセントで税込価格を計算できる() {
// 準備(Arrange)
PriceCalculator calc = new PriceCalculator();
// 実行(Act)
int result = calc.withTax(1000, 10);
// 確認(Assert)
assertEquals(1100, result);
}
}
- テストメソッドには
@Testを付ける - JUnit 5以降、テストクラスやメソッドは
publicでなくてよい assertEquals(期待する値, 実際の値)の順番に注意する(逆にすると、失敗時のメッセージが紛らわしくなる)
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「JUnitの使い方|単体テストの書き方と観点【JUnit 5・6】」
よく使うアサーション
| メソッド | 確認すること |
|---|---|
assertEquals(expected, actual) |
等しい |
assertNotEquals(unexpected, actual) |
等しくない |
assertTrue(condition)・assertFalse(condition) |
真・偽である |
assertNull(obj)・assertNotNull(obj) |
nullである・nullでない |
assertThrows(例外クラス, 処理) |
指定した例外が発生する |
assertDoesNotThrow(処理) |
例外が発生しない |
assertAll(...) |
複数の確認をまとめて行い、すべての失敗を報告する |
assertTimeout(時間, 処理) |
指定時間内に終わる |
例外のテスト
@Test
void 価格がマイナスなら例外になる() {
PriceCalculator calc = new PriceCalculator();
IllegalArgumentException e = assertThrows(
IllegalArgumentException.class,
() -> calc.withTax(-1, 10)
);
assertEquals("価格は0以上", e.getMessage());
}
assertThrows は発生した例外を返すため、メッセージの内容まで確認できます。
複数の確認をまとめる
assertAll(
() -> assertEquals("山田", user.getName()),
() -> assertEquals(30, user.getAge()),
() -> assertTrue(user.isActive())
);
通常の assertEquals は、最初の失敗でテストが止まります。assertAll なら、すべての確認を実行してから、失敗したものをまとめて報告します。
テストの準備と後片付け
| アノテーション | 実行されるタイミング |
|---|---|
@BeforeEach |
各テストメソッドの前に毎回 |
@AfterEach |
各テストメソッドの後に毎回 |
@BeforeAll |
テストクラスの最初に1回(staticメソッド) |
@AfterAll |
テストクラスの最後に1回(staticメソッド) |
class CartTest {
private Cart cart;
@BeforeEach
void setUp() {
cart = new Cart(); // テストごとに新しいカートを用意する
}
@Test
void 商品を追加すると件数が増える() {
cart.add(new Item("ノート", 200));
assertEquals(1, cart.size());
}
}
各テストは、他のテストに影響されずに単独で動くようにします。@BeforeEach で毎回新しい状態を作るのが基本です。
テストの名前と@DisplayName
@Test
@DisplayName("税率10%で、1000円の税込価格は1100円")
void withTax_tenPercent() { ... }
テストの名前は、「何を確認しているか」がわかることが大切です。テストが失敗したとき、名前を見ただけで何が壊れたかがわかるようにします。日本語のメソッド名を使うチームもあります。@DisplayName を使うと、テスト結果に表示される名前を自由に付けられます。
パラメータ化テスト:同じテストを複数の値で
境界値のように、同じ確認を複数の値で行いたいときに便利です。
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
@ParameterizedTest
@CsvSource({
"0, 10, 0",
"1, 10, 1", // 1.1円 → 切り捨てで1円
"9, 10, 9", // 9.9円 → 9円
"10, 10, 11",
"1000, 8, 1080",
})
void 税込価格の計算(int price, int rate, int expected) {
assertEquals(expected, new PriceCalculator().withTax(price, rate));
}
| アノテーション | 値の渡し方 |
|---|---|
@ValueSource(ints = {1, 2, 3}) |
1つの引数に、値を並べて渡す |
@CsvSource({...}) |
複数の引数を、CSV形式で渡す |
@NullAndEmptySource |
null と空文字を渡す |
@EnumSource(Status.class) |
enumのすべての値を渡す |
@MethodSource("メソッド名") |
メソッドが返す値を渡す(複雑なデータ) |
Mockitoで依存するクラスを差し替える
テストしたいクラスが、データベースや外部のAPIを使っている場合、その部分を偽物(モック)に差し替えて、テストしたいクラスの処理だけを確認します。
import static org.mockito.Mockito.*;
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 OrderServiceTest {
@Mock
StockRepository stockRepository; // 偽物に差し替える
@InjectMocks
OrderService orderService; // テスト対象(偽物が注入される)
@Test
void 在庫が足りなければ注文できない() {
when(stockRepository.findQuantity("P01")).thenReturn(0); // 在庫0を返すようにする
assertThrows(OutOfStockException.class,
() -> orderService.order("P01", 1));
verify(stockRepository, never()).decrease(any(), anyInt()); // 在庫は減らされていない
}
}
| 書き方 | 意味 |
|---|---|
when(...).thenReturn(値) |
呼ばれたら、この値を返す |
when(...).thenThrow(例外) |
呼ばれたら、例外を投げる |
verify(mock).メソッド(...) |
そのメソッドが呼ばれたか確認する |
verify(mock, never()).メソッド(...) |
呼ばれていないことを確認する |
モックを使いすぎると、テストが実装の細部に依存し、リファクタリングのたびに壊れるようになります。モックにするのは、データベース・外部API・現在日時など、テストの中で制御しにくいものに絞るのがおすすめです。
AssertJで読みやすく書く
AssertJを使うと、確認の内容を英語の文章のように書けます。
import static org.assertj.core.api.Assertions.*;
assertThat(result).isEqualTo(1100);
assertThat(names).hasSize(3).contains("山田").doesNotContain("田中");
assertThat(user.getEmail()).startsWith("yamada").endsWith("@example.com");
assertThatThrownBy(() -> calc.withTax(-1, 10))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("価格は0以上");
失敗したときのメッセージもわかりやすく、リストや文字列の確認を短く書けるため、実務でよく使われています。
何をテストすべきか:テストの観点
テストの書き方を覚えた後に悩むのが、「どんなテストを書けばよいか」です。次の観点で、テストの値を選びます。
| 観点 | 内容 | 例(税込価格の計算) |
|---|---|---|
| 正常系 | 代表的な値で、期待どおり動くか | 1000円、税率10% |
| 境界値 | 条件が切り替わる値と、その前後 | 0円、1円、上限の価格 |
| 異常系 | 不正な値で、正しくエラーになるか | マイナスの価格、null |
| 端数 | 切り捨て・四捨五入の境目 | 1.1円、9.9円になる価格 |
| 空・0件 | 空のリスト、空文字、0件 | 商品が0件のカート |
| 状態の組み合わせ | 状態によって結果が変わる場合 | 会員・非会員、セール中・通常 |
特に効果が大きいのは、境界値と異常系です。不具合は、条件が切り替わる境目や、想定していない入力で起きやすいためです。
よいテストの特徴
| 特徴 | 内容 |
|---|---|
| 速い | 数秒で全体を実行できる。遅いと実行されなくなる |
| 独立している | 実行する順番や、他のテストの結果に影響されない |
| 何度実行しても同じ結果 | 現在日時や乱数、外部の環境に左右されない |
| 1つのことを確認する | 失敗したとき、原因がすぐにわかる |
| 読みやすい | 準備・実行・確認の3段階がはっきりしている |
現在日時を使う処理は、LocalDate.now() を直接呼ばずに、Clock を外から渡せるようにしておくと、テストで日時を固定できます。
AIでテストを書くときの注意
AIのコーディング支援は、テストコードの作成が得意です。一方で、実装のコードを見て、その動きに合わせたテストを書くため、実装が間違っていても、その間違いを「正しい」と確認するテストになることがあります。
- テストの期待値は、実装ではなく仕様から導かれているかを確認する
- 境界値・異常系のテストが含まれているかを確認する
- AIへの依頼では、仕様(「1円未満は切り捨て」「マイナスはエラー」など)を先に伝える
よくある質問
Q. JUnit 4とJUnit 5の違いは何ですか?
A. パッケージ名(org.junit → org.junit.jupiter.api)、@Before → @BeforeEach、例外のテストの書き方(@Test(expected=...) → assertThrows)などが変わりました。JUnit 5には、JUnit 4のテストを実行する仕組み(Vintage)もあり、段階的に移行できます。
Q. privateメソッドはテストすべきですか? A. 基本的には、publicメソッドを通してテストします。privateメソッドを直接テストしたくなる場合は、その処理を別のクラスに切り出すことを検討します。
Q. テストのカバレッジ(網羅率)は何%を目指すべきですか? A. 数値そのものより、重要な処理と境界値・異常系がテストされているかが大切です。カバレッジはJaCoCoなどのツールで測定でき、テストされていない箇所を見つける手がかりとして使います。
Q. データベースを使う処理もJUnitでテストできますか? A. できます。モックに差し替える方法のほか、Testcontainersで本物のデータベースをコンテナで起動してテストする方法もあります(こちらは結合テストの位置づけになります)。
まとめ
- JUnitは、Javaの単体テストの定番。JUnit 5の書き方はJUnit 6でも共通
- Mavenは junit-jupiter、Gradleは useJUnitPlatform() を忘れずに
- 基本は @Test + assertEquals(期待値が先)、例外は assertThrows
- 複数の値で試すなら @ParameterizedTest、依存の差し替えは Mockito、読みやすさは AssertJ
- テストの値は、正常系・境界値・異常系・端数・0件の観点で選ぶ
- AIが書いたテストは、期待値が仕様から導かれているかを確認する
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「JUnitの使い方|単体テストの書き方と観点【JUnit 5・6】」
株式会社RJCでは、テストを「品質を確かめ、安心して変更するための仕組み」として大切にしています。こうした知識を仲間と共有しながら、一緒に成長を楽しんでいます。
株式会社RJC ― SI事業・SES事業・AI駆動開発。未経験からエンジニアに育てるのが得意な会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。