Spring Bootでテストを書こう - JUnitとMockitoで始める単体テスト入門


Spring Bootでアプリケーション開発ができるようになったけれど、テストコードはまだ書いたことがない――そんな方は多いのではないでしょうか。

この記事では、Spring Bootアプリケーションで初めてテストコードを書く方に向けて、JUnitとMockitoを使ったController層とService層の単体テストの書き方を段階的に解説します。記事を読み終えた後には、自分のプロジェクトで基本的なテストコードを独力で書けるようになることを目指します。

なぜSpring Bootアプリケーションにテストが必要なのか

実務でテストコードを書く理由は、主に以下の3つです。

手動テストの限界

アプリケーションが成長するにつれて、すべての機能を手動でテストするのは現実的ではなくなります。自動テストがあれば、コマンド一つで数秒〜数分ですべてのテストを実行でき、変更が既存機能を壊していないか即座に確認できます。

リファクタリングや機能追加時の安全網

コードをリファクタリングしたり、新しい機能を追加したりする際、既存の機能が正しく動作し続けることを保証する必要があります。テストコードは、変更によって意図しない不具合が混入していないかを検知する「安全網」として機能します。CI/CDパイプラインに組み込むことで、本番環境へのリリース前に自動的に品質を検証できます。

バグの早期発見によるコスト削減

バグは開発の早い段階で見つけるほど、修正コストが低くなります。テストコードによって開発中にバグを発見できれば、本番環境にリリースされてから発見されるよりもはるかに低コストで対処できます。実務では、テストコードを書くスキルは必須です。

Spring Bootのテストに使う主要なツール

Spring Bootでテストを書く際には、いくつかのツールを組み合わせて使います。それぞれの役割を理解しましょう。

単体テストとは

まず、この記事で扱う「単体テスト」の定義を明確にしておきます。単体テストとは、アプリケーションの最小単位(クラスやメソッド)を、他の依存から切り離してテストすることを指します。依存するコンポーネントはモックで置き換えることで、テスト対象の振る舞いだけに集中できます。

なお、後述する@WebMvcTestによるController層のテストは、厳密にはSpring MVCの機能を利用するため「統合テスト寄りの単体テスト」という性質を持ちますが、本記事では初学者向けに「Controller層の単体テスト」として扱います。

JUnit 5の役割

JUnit 5は、Javaのテストを実行するためのフレームワークです。テストメソッドを定義し、実行し、結果を報告する基盤を提供します。@Testアノテーションを付けたメソッドがテストとして認識され、アサーション(検証)が失敗するとテストが失敗します。

Mockitoの役割

Mockitoは、依存するオブジェクトの「モック(偽物)」を作成するためのライブラリです。例えば、Service層をテストする際に、実際のデータベースにアクセスするRepositoryを使うのではなく、モックのRepositoryを使うことで、データベースなしでService層のロジックだけをテストできます。

Spring Boot Test Starter

Spring Boot Test Starterは、Spring Bootアプリケーションのテストに必要なライブラリをまとめて提供します。JUnit 5、Mockito、Spring Test、AssertJなどが含まれています。

pom.xmlに以下の依存関係を追加することで利用できます。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

@SpringBootTestと@WebMvcTestの使い分け

Spring Bootには、テストの目的に応じて使い分けるべきアノテーションがあります。

  • @SpringBootTest: Spring Bootアプリケーション全体を起動してテストします。統合テストに向いていますが、起動に時間がかかります。
  • @WebMvcTest: Controller層だけをテストするために、必要最小限のコンポーネントだけを起動します。軽量で高速です。

単体テストでは、テスト対象の層だけを起動する@WebMvcTestを使うことで、テストの実行速度を大幅に改善できます。

@MockBeanと@Autowiredの使い分け

  • @Autowired: 実際のBeanをDIコンテナから注入します。
  • @MockBean: モックのBeanを作成し、DIコンテナに登録します。既存の同じ型のBeanがあれば置き換えられます。

@WebMvcTestでController層をテストする際、Controllerが依存するServiceは実際には動かす必要がないため、@MockBeanでモック化します。

テスト対象のサンプルアプリケーションを準備する

テストを書く前に、テスト対象となるシンプルなユーザー管理APIを実装します。

pom.xmlの依存関係設定

まず、必要な依存関係を確認しましょう。以下は、テストに必要な最小限の設定を含むpom.xmlの例です。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
        <relativePath/>
    </parent>
    
    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    
    <properties>
        <java.version>17</java.version>
    </properties>
    
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

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ベースの実装を使用しています。

package com.example.demo.repository;

import com.example.demo.model.User;
import org.springframework.stereotype.Repository;
import java.util.*;

@Repository
public class UserRepository {
    private final Map<Long, User> store = new HashMap<>();
    private Long idCounter = 1L;

    public User save(User user) {
        if (user.getId() == null) {
            user.setId(idCounter++);
        }
        store.put(user.getId(), user);
        return user;
    }

    public Optional<User> findById(Long id) {
        return Optional.ofNullable(store.get(id));
    }

    public List<User> findAll() {
        return new ArrayList<>(store.values());
    }
}

UserNotFoundException

package com.example.demo.exception;

public class UserNotFoundException extends RuntimeException {
    public UserNotFoundException(String message) {
        super(message);
    }
}

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を実装します。

package com.example.demo.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleUserNotFoundException(UserNotFoundException ex) {
        ErrorResponse error = new ErrorResponse("USER_NOT_FOUND", ex.getMessage());
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
    }

    public static class ErrorResponse {
        private String code;
        private String message;

        public ErrorResponse(String code, String message) {
            this.code = code;
            this.message = message;
        }

        public String getCode() { return code; }
        public void setCode(String code) { this.code = code; }
        public String getMessage() { return message; }
        public void setMessage(String message) { this.message = message; }
    }
}

詳しい例外ハンドリングの実装方法については、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

package com.example.demo.dto;

public class UserCreateRequest {
    private String name;
    private String email;

    // ゲッター・セッター
    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; }
}

このアプリケーションでは、各層が明確に責務を分担しています。

  • 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

  1. テストクラスまたはテストメソッドを右クリック
  2. 「Run ‘テスト名’」を選択
  3. 画面下部にテスト結果が表示されます

Eclipse

  1. テストクラスまたはテストメソッドを右クリック
  2. 「Run As」→「JUnit Test」を選択
  3. 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: スキップされたテストの数

テストが失敗すると、どのテストが失敗したか、どのアサーションが失敗したかが詳細に表示されます。

よくあるエラーとその対処法

テストを書いていると、いくつかの典型的なエラーに遭遇することがあります。ここでは、初心者がつまずきやすいエラーとその対処法を紹介します。

「No qualifying bean of type」エラー

エラーメッセージ例:

org.springframework.beans.factory.NoSuchBeanDefinitionException:
No qualifying bean of type 'com.example.demo.service.UserService' available:
expected at least 1 bean which qualifies as autowire candidate.

原因: @WebMvcTestを使っている場合、Controller以外のコンポーネント(ServiceやRepositoryなど)は自動的にはBeanとして登録されません。

対処法: 依存するServiceやRepositoryを@MockBeanでモック化します。

@WebMvcTest(UserController.class)
class UserControllerTest {
    @MockBean  // これを忘れずに追加
    private UserService userService;
}

NullPointerExceptionが発生する場合

エラーメッセージ例:

java.lang.NullPointerException: Cannot invoke "com.example.demo.model.User.getId()" 
because the return value of "com.example.demo.service.UserService.getUserById(Long)" is null

原因: モックの振る舞いを定義していないメソッドが呼ばれると、デフォルトではnullが返されます。

対処法: 使用するすべてのモックメソッドに対して、when().thenReturn()で振る舞いを定義します。

// NG: モックの振る舞いが定義されていない
User user = userService.getUserById(1L); // nullが返される

// OK: 振る舞いを定義
when(userService.getUserById(1L)).thenReturn(new User(1L, "太郎", "[email protected]"));
User user = userService.getUserById(1L); // Userオブジェクトが返される

@MockBeanと@Mockの使い分けミス

@Mock: Mockitoの機能。Springコンテキストとは無関係にモックを作成します。 @MockBean: Spring Bootの機能。モックをSpring ApplicationContextに登録します。

使い分けのルール:

  • @ExtendWith(MockitoExtension.class)を使う純粋なMockitoテストでは@Mockを使う
  • @WebMvcTestや@SpringBootTestを使うSpring統合テストでは@MockBeanを使う

テストコンテキストの起動に失敗する場合

エラーメッセージ例:

java.lang.IllegalStateException: Failed to load ApplicationContext
Caused by: org.springframework.beans.factory.BeanCreationException: 
Error creating bean with name 'dataSource'

原因: application.propertiesやapplication.ymlの設定が不足している、または間違っている可能性があります。

対処法:

  • テスト用の設定ファイル(src/test/resources/application.yml)を作成する
  • @TestPropertySourceを使ってテスト専用の設定を上書きする
@WebMvcTest(UserController.class)
@TestPropertySource(properties = {
    "spring.datasource.url=jdbc:h2:mem:testdb"
})
class UserControllerTest {
    // ...
}

モックの振る舞いが反映されない場合

確認ポイント:

  1. モックの振る舞い定義と実際に呼ばれるメソッドの引数が一致しているか確認
  2. when()の引数とテスト実行時の引数が完全に一致しない場合は、any()eq()などのマッチャーを使う
// 厳密な一致が必要な場合
when(userService.getUserById(1L)).thenReturn(user);

// 任意の引数を受け付ける場合
when(userService.getUserById(anyLong())).thenReturn(user);

まとめと次のステップ

この記事では、Spring BootアプリケーションでController層とService層の単体テストを書く基本的なパターンを学びました。

学んだ内容の振り返り

  • Service層のテスト: @MockでRepositoryをモック化し、Serviceのビジネスロジックを独立してテストする
  • Controller層のテスト: @WebMvcTestとMockMvcを使い、HTTPリクエスト・レスポンスを検証する
  • モックの活用: @MockBeanと@Mockを適切に使い分けることで、依存コンポーネントを切り離してテストする
  • 正常系・異常系の両方をテスト: 期待通りの動作だけでなく、エラーケースも検証する

次のステップ

この記事で扱ったのは単体テストの基礎です。さらなる学習として、以下のトピックに取り組むことをお勧めします。

  • Repository層のテスト: @DataJpaTestを使ったデータベース連携のテスト(今後の記事で扱う予定)
  • 統合テスト: @SpringBootTestを使った、複数の層をまたがるテスト(今後の記事で扱う予定)
  • テストカバレッジの測定: JaCoCoなどのツールを使った網羅率の可視化
  • テストデータのビルダーパターン: テストコードの可読性向上

実務で役立つテストのベストプラクティス

  • テストは小さく保つ: 1つのテストメソッドで1つの観点だけを検証する
  • テスト名は説明的に: shouldReturnUser_whenUserExistsのように、何をテストしているかが一目で分かる名前をつける
  • Given-When-Thenパターン: テストコードを「準備(Given)」「実行(When)」「検証(Then)」の3つのセクションに分けると読みやすくなる
  • テストの独立性を保つ: テスト間で状態を共有せず、それぞれが独立して実行できるようにする

テストコードは、将来の自分や他の開発者への「ドキュメント」でもあります。読みやすく、メンテナンスしやすいテストコードを心がけましょう。


よくある質問

@SpringBootTestと@WebMvcTestはどう使い分ければいいですか?

@WebMvcTestは、Controller層だけを単体テストする際に使います。必要最小限のコンポーネントだけを起動するため、高速です。一方、@SpringBootTestは、アプリケーション全体を起動する統合テストに使います。複数の層をまたがる動作を検証したい場合や、実際のデータベースとの連携をテストしたい場合に適しています。

基本的には、単体テストでは@WebMvcTestを使い、統合テストでは@SpringBootTestを使うと覚えておくとよいでしょう。

@MockBeanと@Mockの違いは何ですか?

@MockはMockitoが提供するアノテーションで、純粋なモックオブジェクトを作成します。Spring ApplicationContextとは無関係に動作します。

@MockBeanはSpring Bootが提供するアノテーションで、モックをSpring ApplicationContextにBeanとして登録します。既存の同じ型のBeanがあれば、モックで置き換えられます。

@WebMvcTestや@SpringBootTestなど、Springコンテキストを起動するテストでは@MockBeanを使い、純粋なMockitoテスト(@ExtendWith(MockitoExtension.class))では@Mockを使います。

統合テストと単体テストの違いは何ですか?

単体テストは、1つのクラスやメソッドを他の依存から切り離して、その動作だけを検証するテストです。依存するコンポーネントはモックで置き換えます。実行速度が速く、問題の原因を特定しやすいのが特徴です。

統合テストは、複数のコンポーネントを組み合わせて、それらが正しく連携するかを検証するテストです。実際のデータベースや外部APIと接続することもあります。より本番環境に近い状態でテストできますが、実行速度は遅くなります。

一般的には、単体テストを多く書き、重要な統合ポイントだけを統合テストでカバーする「テストピラミッド」の考え方が推奨されています。

テストコードはどこに配置すればいいですか?

テストコードは、src/test/javaディレクトリ配下に配置します。パッケージ構造は、本番コード(src/main/java)と同じにするのが一般的です。

例:

src/main/java/com/example/demo/controller/UserController.java
src/test/java/com/example/demo/controller/UserControllerTest.java

テスト用のリソースファイル(application.ymlなど)は、src/test/resourcesに配置します。

Repository層のテストはどう書けばいいですか?

Repository層のテストには、@DataJpaTestアノテーションを使います。これにより、JPA関連のコンポーネントだけが起動され、インメモリデータベース(H2など)を使った高速なテストが可能になります。

基本的な例:

@DataJpaTest
class UserRepositoryTest {
    @Autowired
    private UserRepository userRepository;
    
    @Test
    void findById_shouldReturnUser() {
        // テストコード
    }
}

Repository層のテストについては、今後の記事で詳しく解説する予定です。

テストカバレッジはどれくらい目指すべきですか?

テストカバレッジ(コードのうち、テストで実行された部分の割合)は、一般的に70〜80%程度を目指すのが現実的です。100%を目指すと、費用対効果が低いテストまで書くことになりがちです。

重要なのは、カバレッジの数値よりも、ビジネスロジックの重要な部分や、バグが起きやすい複雑な処理が適切にテストされているかです。

MockMvcとRestAssuredの違いは何ですか?

MockMvcは、Spring MVCのテスト用ツールで、サーブレットコンテナを起動せずにControllerをテストできます。軽量で高速です。

RestAssuredは、実際のHTTPリクエストを送信するテストライブラリです。より本番環境に近い統合テストに向いていますが、実行速度はMockMvcより遅くなります。

単体テストではMockMvcを使い、E2Eテストや統合テストではRestAssuredを使うのが一般的です。