Spring Bootでアプリケーション開発ができるようになったけれど、テストコードはまだ書いたことがない。そんな方は多いのではないでしょうか。
この記事では、Spring Bootアプリケーションで初めてテストコードを書く方に向けて、JUnitとMockitoを使ったController層とService層の単体テストの書き方を段階的に解説します。記事を読み終えた後には、自分のプロジェクトで基本的なテストコードを独力で書けるようになることを目指します。
なぜSpring Bootアプリケーションにテストが必要なのか
実務でテストコードを書く理由は主に3つです。
まず、アプリケーションが成長すると手動テストでは限界があります。自動テストなら、コマンド一つで数秒〜数分ですべての機能を検証できますよね。
次に、リファクタリングや機能追加の際の安全網になります。テストコードがあれば、変更によって既存機能が壊れていないかを即座に確認できます。
最後に、バグの早期発見でコスト削減につながります。開発中にバグを見つけられれば、本番環境にリリースされてから発見されるよりもはるかに低コストで対処できます。
Spring Bootのテストに使う主要なツール
単体テストとは
単体テストとは、アプリケーションの最小単位(クラスやメソッド)を、他の依存から切り離してテストすることです。依存するコンポーネントはモックで置き換えることで、テスト対象の振る舞いだけに集中できます。
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を使います。
まとめ
この記事では、JUnitとMockitoを使った単体テストの基本を学びました。
- Service層のテスト: @MockでRepositoryをモック化し、ビジネスロジックを独立してテストする
- Controller層のテスト: @WebMvcTestとMockMvcを使い、HTTPリクエスト・レスポンスを検証する
- モックの使い分け: @MockBeanと@Mockを適切に使い分ける
- 正常系・異常系の両方をテスト: エラーケースも忘れずに検証する
実務では、テスト名を説明的にし、Given-When-Thenパターンで読みやすく書くことが大切です。Repository層のテストや統合テストについては、今後の記事で詳しく解説する予定です。