Spring Bootでアプリケーション開発ができるようになったけれど、テストコードはまだ書いたことがない。そんな方は多いのではないでしょうか。
この記事では、Spring Bootアプリケーションで初めてテストコードを書く方に向けて、JUnitとMockitoを使ったController層とService層の単体テストの書き方を段階的に解説します。記事を読み終えた後には、自分のプロジェクトで基本的なテストコードを独力で書けるようになることを目指します。
なぜSpring Bootアプリケーションにテストが必要なのか
実務でテストコードを書く理由は主に3つです。
まず、アプリケーションが成長すると手動テストでは限界があります。自動テストなら、コマンド一つで数秒〜数分ですべての機能を検証できますよね。
次に、リファクタリングや機能追加の際の安全網になります。テストコードがあれば、変更によって既存機能が壊れていないかを即座に確認できます。
最後に、バグの早期発見でコスト削減につながります。開発中にバグを見つけられれば、本番環境にリリースされてから発見されるよりもはるかに低コストで対処できます。
Spring Bootのテストに使う主要なツール
JUnitとSpring Bootテストサポートの関係
『JUnit vs Spring Boot』という比較で検索される方も多いですが、これらは競合関係ではなく、役割が異なります。
- JUnit 5: テストの実行エンジン。
@Testの認識、アサーション、ライフサイクル管理を担う基盤。 - Spring Boot Test (
spring-boot-starter-test): JUnit 5の上に乗る形で、@SpringBootTestや@WebMvcTestなどSpring特有のテストサポートを提供。 - Mockito: 依存オブジェクトをモック化するライブラリ。JUnitとは独立に動作し、
@MockitoExtensionまたは@MockBeanで利用。
つまり、Spring BootアプリケーションのテストはJUnit 5を土台に、Spring Boot Test + Mockitoを組み合わせて書きます。JUnit 4を使っている既存プロジェクトもありますが、Spring Boot 2.4以降はJUnit 5がデフォルトです。
単体テストとは
単体テストとは、アプリケーションの最小単位(クラスやメソッド)を、他の依存から切り離してテストすることです。依存するコンポーネントはモックで置き換えることで、テスト対象の振る舞いだけに集中できます。
JUnit 5とMockito
JUnit 5 は、Javaのテストを実行するためのフレームワークです。@Testアノテーションを付けたメソッドがテストとして認識されます。
Mockito は、依存するオブジェクトの「モック(偽物)」を作成するライブラリです。例えば、Service層をテストする際に、実際のRepositoryではなくモックを使うことで、データベースなしでロジックだけをテストできます。
これらはspring-boot-starter-testに含まれているので、特別な設定は不要です。
@WebMvcTestと@MockBeanの使い分け
- @WebMvcTest: Controller層だけをテストするために、必要最小限のコンポーネントだけを起動します。軽量で高速です。
- @MockBean: モックのBeanを作成し、DIコンテナに登録します。@WebMvcTestでは、Controllerが依存するServiceを@MockBeanでモック化します。
単体テストでは、テスト対象の層だけを起動する@WebMvcTestを使うことで、テストの実行速度を大幅に改善できます。
テスト対象のサンプルアプリケーションを準備する
テストを書く前に、テスト対象となるシンプルなユーザー管理APIを実装します。
pom.xmlの依存関係設定
Spring Boot 3.2以降のプロジェクトなら、spring-boot-starter-testが自動的に含まれています。念のため、pom.xmlに以下の依存関係があることを確認しましょう。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
Userエンティティ
package com.example.demo.model;
public class User {
private Long id;
private String name;
private String email;
// コンストラクタ
public User(Long id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
// ゲッター・セッター
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
}
UserRepository
実務ではSpring Data JPAのRepositoryインターフェースを使用しますが、ここではテストの説明を簡潔にするためMapベースのシンプルな実装を使用します。save()、findById()、findAll()の3つのメソッドを持つ@Repositoryクラスとして実装してください。
UserNotFoundException
カスタム例外クラスとして、RuntimeExceptionを継承したUserNotFoundExceptionを作成します。
UserService
Serviceはビジネスロジックを担当します。ここでは、ユーザーが存在しない場合に例外をスローする処理を含めています。
package com.example.demo.service;
import com.example.demo.exception.UserNotFoundException;
import com.example.demo.model.User;
import com.example.demo.repository.UserRepository;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User createUser(String name, String email) {
User user = new User(null, name, email);
return userRepository.save(user);
}
public User getUserById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found: " + id));
}
public List<User> getAllUsers() {
return userRepository.findAll();
}
}
GlobalExceptionHandler
Controller層の異常系テストを動作させるために、@RestControllerAdviceで例外をハンドリングします。UserNotFoundExceptionを@ExceptionHandlerで受け取り、404ステータスとエラーレスポンスを返すように実装してください。詳しい例外ハンドリングの実装方法については、Spring BootのREST APIで例外をハンドリングする方法の記事で解説しています。
UserController
ControllerはHTTPリクエストを受け取り、Serviceを呼び出して結果を返します。
package com.example.demo.controller;
import com.example.demo.dto.UserCreateRequest;
import com.example.demo.model.User;
import com.example.demo.service.UserService;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return userService.getUserById(id);
}
@GetMapping
public List<User> getAllUsers() {
return userService.getAllUsers();
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public User createUser(@RequestBody UserCreateRequest request) {
return userService.createUser(request.getName(), request.getEmail());
}
}
UserCreateRequest
POSTリクエストのボディを受け取るDTOクラスです。nameとemailフィールドを持つシンプルなPOJOとして実装してください。
このアプリケーションでは、各層が明確に責務を分担しています。
- Controller: HTTPリクエストのハンドリング
- Service: ビジネスロジック
- Repository: データの永続化
各層は依存性注入(DI)によって疎結合に保たれており、テストしやすい設計になっています。Controllerは@Componentの特殊化である@RestControllerによってSpringのコンポーネントとして管理されています。
Service層の単体テストを書く
まず、Service層のテストから始めましょう。Service層のテストでは、Repositoryをモック化して、Serviceのビジネスロジックだけをテストします。
基本的なServiceテストの構造
package com.example.demo.service;
import com.example.demo.exception.UserNotFoundException;
import com.example.demo.model.User;
import com.example.demo.repository.UserRepository;
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;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
void getUserById_shouldReturnUser_whenUserExists() {
// Given: テストデータの準備
Long userId = 1L;
User expectedUser = new User(userId, "太郎", "[email protected]");
when(userRepository.findById(userId)).thenReturn(Optional.of(expectedUser));
// When: テスト対象のメソッド実行
User actualUser = userService.getUserById(userId);
// Then: 結果の検証
assertNotNull(actualUser);
assertEquals(expectedUser.getId(), actualUser.getId());
assertEquals(expectedUser.getName(), actualUser.getName());
assertEquals(expectedUser.getEmail(), actualUser.getEmail());
verify(userRepository, times(1)).findById(userId);
}
}
コードの解説
- @ExtendWith(MockitoExtension.class): JUnit 5でMockitoを使うための設定です。
- @Mock: モックオブジェクトを作成します。ここではUserRepositoryのモックを作成しています。
- @InjectMocks: モックを注入したテスト対象のオブジェクトを作成します。UserServiceに@MockのUserRepositoryが自動的に注入されます。
- when().thenReturn(): モックの振る舞いを定義します。
userRepository.findById(userId)が呼ばれたときに、指定したUserオブジェクトを返すように設定しています。 - verify(): メソッドが期待通りに呼ばれたかを検証します。ここでは
findByIdが1回だけ呼ばれたことを確認しています。
異常系のテスト
正常系だけでなく、異常系のテストも重要です。ユーザーが見つからない場合の挙動をテストします。
@Test
void getUserById_shouldThrowException_whenUserNotFound() {
// Given
Long userId = 999L;
when(userRepository.findById(userId)).thenReturn(Optional.empty());
// When & Then
UserNotFoundException exception = assertThrows(
UserNotFoundException.class,
() -> userService.getUserById(userId)
);
assertTrue(exception.getMessage().contains("User not found"));
verify(userRepository, times(1)).findById(userId);
}
assertThrows を使うと、特定の例外がスローされることを検証できます。
createUserのテスト
@Test
void createUser_shouldSaveAndReturnUser() {
// Given
String name = "太郎";
String email = "[email protected]";
User savedUser = new User(1L, name, email);
when(userRepository.save(any(User.class))).thenReturn(savedUser);
// When
User result = userService.createUser(name, email);
// Then
assertNotNull(result);
assertEquals(1L, result.getId());
assertEquals(name, result.getName());
assertEquals(email, result.getEmail());
verify(userRepository, times(1)).save(any(User.class));
}
any(User.class) を使うと、任意のUserオブジェクトが渡された場合の振る舞いを定義できます。
Controller層の単体テストを書く
Controller層のテストでは、HTTPリクエストとレスポンスが正しく処理されるかを検証します。@WebMvcTestとMockMvcを使います。
基本的なControllerテストの構造
package com.example.demo.controller;
import com.example.demo.dto.UserCreateRequest;
import com.example.demo.exception.UserNotFoundException;
import com.example.demo.model.User;
import com.example.demo.service.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
void getUser_shouldReturnUser_whenUserExists() throws Exception {
// Given
Long userId = 1L;
User user = new User(userId, "太郎", "[email protected]");
when(userService.getUserById(userId)).thenReturn(user);
// When & Then
mockMvc.perform(get("/api/users/{id}", userId))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("太郎"))
.andExpect(jsonPath("$.email").value("[email protected]"));
verify(userService, times(1)).getUserById(userId);
}
}
コードの解説
- @WebMvcTest(UserController.class): UserControllerだけをテストするために、必要最小限のコンポーネントだけを起動します。
- @Autowired MockMvc: HTTPリクエストをシミュレートするためのツールです。
- @MockBean UserService: UserServiceをモック化し、Spring ApplicationContextに登録します。
- mockMvc.perform(): HTTPリクエストをシミュレートします。
- andExpect(): レスポンスの検証を行います。ステータスコード、Content-Type、JSONの内容などを検証できます。
- jsonPath(): JSONレスポンスの特定のフィールドを検証します。
POSTリクエストのテスト
@Test
void createUser_shouldReturnCreatedUser() throws Exception {
// Given
User createdUser = new User(1L, "太郎", "[email protected]");
when(userService.createUser("太郎", "[email protected]")).thenReturn(createdUser);
// When & Then
mockMvc.perform(
post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"太郎\",\"email\":\"[email protected]\"}")
)
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("太郎"))
.andExpect(jsonPath("$.email").value("[email protected]"));
verify(userService, times(1)).createUser("太郎", "[email protected]");
}
POSTリクエストでは、.contentType()でContent-Typeを指定し、.content()でリクエストボディを設定します。また、.andExpect(status().isCreated())で201ステータスコードが返されることを検証しています。
バリデーションを含むリクエストのテストについては、@Valid アノテーションでバリデーションを実装する方法も参考にしてください。
異常系のテスト(404エラー)
@Test
void getUser_shouldReturn404_whenUserNotFound() throws Exception {
// Given
Long userId = 999L;
when(userService.getUserById(userId))
.thenThrow(new UserNotFoundException("User not found: " + userId));
// When & Then
mockMvc.perform(get("/api/users/{id}", userId))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.code").value("USER_NOT_FOUND"))
.andExpect(jsonPath("$.message").value("User not found: 999"));
verify(userService, times(1)).getUserById(userId);
}
このテストは、前述のGlobalExceptionHandlerが実装されていることで正しく動作します。@RestControllerAdviceによって例外が適切にハンドリングされ、404ステータスとエラーレスポンスが返されます。
テストを実行する
テストコードを書いたら、実際に実行して結果を確認しましょう。
IDEでテストを実行する
IntelliJ IDEA
- テストクラスまたはテストメソッドを右クリック
- 「Run ‘テスト名’」を選択
- 画面下部にテスト結果が表示されます
Eclipse
- テストクラスまたはテストメソッドを右クリック
- 「Run As」→「JUnit Test」を選択
- JUnitビューにテスト結果が表示されます
Mavenコマンドでテストを実行する
コマンドラインからすべてのテストを実行するには、プロジェクトのルートディレクトリで以下のコマンドを実行します。
./mvnw test
Windowsの場合は、mvnw.cmd testを実行します。
特定のテストだけを実行する
特定のテストクラスだけを実行したい場合は、以下のようにします。
./mvnw test -Dtest=UserServiceTest
特定のテストメソッドだけを実行する場合は、以下のようにします。
./mvnw test -Dtest=UserServiceTest#getUserById_shouldReturnUser_whenUserExists
テスト結果の読み方
テストが成功すると、以下のような出力が表示されます。
[INFO] Tests run: 6, Failures: 0, Errors: 0, Skipped: 0
- Tests run: 実行されたテストの総数
- Failures: アサーションが失敗したテストの数
- Errors: 予期しないエラーが発生したテストの数
- Skipped: スキップされたテストの数
テストが失敗すると、どのテストが失敗したか、どのアサーションが失敗したかが詳細に表示されます。
テスト実装時のポイント
テストを書いていてつまずきやすいのは、モックの扱いです。@WebMvcTestを使う場合、Controllerが依存するServiceは@MockBeanでモック化する必要があります。また、モックの振る舞いをwhen().thenReturn()で定義しないと、デフォルトでnullが返されてNullPointerExceptionが発生します。
@Mockと@MockBeanの使い分けも重要です。Mockito単体でのテストでは@Mock、Springコンテキストを起動するテストでは@MockBeanを使います。
例外ハンドラのテストでは、ProblemDetailのカスタムフィールドや MDC によるトレース ID 付与の検証が必要になることもあります。本番品質の例外ハンドラ設計についてはSpring BootのGlobalExceptionHandlerを本番運用向けに実装するで詳しく解説しています。
また、リクエストDTOにカスタムバリデーションを掛けるケースでは、テスト戦略も変わります。独自バリデーションの実装とテスト方法はSpring Bootでカスタムバリデーションアノテーションを作る方法、イベント駆動な処理のテストはSpring BootのApplicationEventでモジュール間を疎結合にする方法も合わせてご確認ください。
まとめ
この記事では、JUnitとMockitoを使った単体テストの基本を学びました。
- Service層のテスト: @MockでRepositoryをモック化し、ビジネスロジックを独立してテストする
- Controller層のテスト: @WebMvcTestとMockMvcを使い、HTTPリクエスト・レスポンスを検証する
- モックの使い分け: @MockBeanと@Mockを適切に使い分ける
- 正常系・異常系の両方をテスト: エラーケースも忘れずに検証する
実務では、テスト名を説明的にし、Given-When-Thenパターンで読みやすく書くことが大切です。Repository層のテストや統合テストについては、今後の記事で詳しく解説する予定です。
@WebMvcTestと@SpringBootTestの使い分け
単体テストでは@WebMvcTestを使いましたが、Spring Bootには他にも複数のテストアノテーションがあります。それぞれが起動するコンテキストの範囲が異なるため、目的に応じて使い分けが必要です。
| アノテーション | 起動範囲 | 主な用途 | 速度 |
|---|---|---|---|
@WebMvcTest | Controller層のみ(MVC関連Beanのみ) | Controller単体テスト | 高速 |
@DataJpaTest | JPA関連Beanのみ(Repository + H2など) | Repository単体テスト | 高速 |
@SpringBootTest | アプリ全体のApplicationContext | 統合テスト・E2E | 低速 |
@SpringBootTestによる統合テストの例
@SpringBootTest
@AutoConfigureMockMvc
class UserIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Test
void createAndGetUser_endToEnd() throws Exception {
// Controllerから実DBまで通しでテスト
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"花子\",\"email\":\"[email protected]\"}"))
.andExpect(status().isCreated());
}
}
@SpringBootTestは本物のApplicationContextを起動するため、Controller→Service→Repositoryの結合を検証できますが、起動に数秒かかります。普段は@WebMvcTest/@DataJpaTestで書き、結合の確認だけ@SpringBootTestを使うのがバランスの良い構成です。
@DataJpaTestでRepository層をテストする
Repository層は@DataJpaTestを使うと、JPA関連のBeanとインメモリDB(H2)だけが起動され、高速にRepositoryの動作を検証できます。
@DataJpaTest
class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
@Test
void findByEmail_shouldReturnUser_whenEmailExists() {
User saved = userRepository.save(new User(null, "太郎", "[email protected]"));
Optional<User> found = userRepository.findByEmail("[email protected]");
assertTrue(found.isPresent());
assertEquals(saved.getId(), found.get().getId());
}
}
@DataJpaTestはデフォルトでテストごとにトランザクションをロールバックするため、テスト間の独立性が保たれます。
テストカバレッジをJaCoCoで計測する
テストを書いたら、どれだけコードがカバーされているかを計測しましょう。JaCoCoはJavaで最も使われるカバレッジ計測ツールで、Maven/Gradleどちらにも組み込めます。
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.11</version>
<executions>
<execution>
<goals><goal>prepare-agent</goal></goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals><goal>report</goal></goals>
</execution>
</executions>
</plugin>
./mvnw testを実行すると、target/site/jacoco/index.htmlにHTMLレポートが出力されます。クラス・メソッド・行・分岐ごとのカバレッジが確認できます。実務では行カバレッジ70-80%を目標にしつつ、重要なビジネスロジックは分岐カバレッジも意識するのが現実的です。
よくある質問 (FAQ)
@Mockと@MockBeanはどう使い分ける?
- @Mock: Mockito単体で動作。Springコンテキストを起動しない軽量テスト(
@ExtendWith(MockitoExtension.class))で使います。 - @MockBean: Spring TestがApplicationContextに登録するモック。
@WebMvcTestや@SpringBootTestなど、Springコンテキストを起動するテストで使います。
JUnit 4とJUnit 5の違いは?
Spring Boot 2.4以降のデフォルトはJUnit 5です。@RunWith→@ExtendWith、@Before→@BeforeEachなど主要アノテーションが変わっています。新規プロジェクトでは迷わずJUnit 5を選択しましょう。
@WebMvcTestでServiceが@Autowiredできない
@WebMvcTestはController層しか起動しないため、Service Beanは存在しません。@MockBeanでモック化するのが正しい使い方です。実Serviceを使いたい場合は@SpringBootTest+@AutoConfigureMockMvcを選びましょう。
モックの戻り値を設定しないとどうなる?
Mockitoのモックは、戻り値をwhen().thenReturn()で定義しない場合、参照型はnull・プリミティブは0/falseを返します。これによりNullPointerExceptionが発生することが多いため、テスト対象が呼ぶメソッドの戻り値は必ず設定しましょう。
Spring Boot Testの実行が遅いときは?
@SpringBootTestを多用するとコンテキストの起動回数が増えて遅くなります。@WebMvcTest/@DataJpaTestにスライス化する、@MockBeanの組み合わせを揃えてコンテキストキャッシュを効かせる、の2点で大幅に改善します。