2026年5月28日木曜日

SpringBoot でユニットテストとコンテナを使ったインテグレーションテストを作成する

SpringBoot でユニットテストとコンテナを使ったインテグレーションテストを作成する

概要

前回 DTO + Service レイヤーを追加しました
アプリがこの辺りまで大きくなったら一度テストを追加することをオススメします

SpringBoot にはコンテナを使って MySQL を起動しそれをインテグレーションテスト用として使えます

今回は MySQL を使わないでモックするユニットテストと MySQL コンテナを起動し実際にデータベースを使ったインテグレーションテストを追加する方法を紹介します

環境

  • macOS 26.4.1
  • openjdk 26.0.1
  • SpringBoot 4.0.6
  • jasypt-spring-boot 4.0.4
  • gradle 9.5.1
  • VSCode 1.121.0
  • MySQL 9.6.0

コントローラのユニットテストの追加

コントローラでは Service をモックします
Service が期待する値をモックにセットしあとは実際にリクエストを送信しその値が返ってくるか検証します

テスト内では検証用の static method を大量に使うのでそこはアスタリスクでインポートしています

  • vim src/test/java/com/example/demo/MainControllerTest.java
package com.example.demo;

import java.util.List;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
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.*;

import com.example.demo.dto.UserResponse;
import com.example.demo.service.UserService;

@WebMvcTest(MainController.class)
class MainControllerTest {

	@Autowired
	private MockMvc mockMvc;

	@MockitoBean
	private UserService userService;

	@Test
	void ユーザー作成が成功する() throws Exception {
		String json = """
				{
				    "name": "taro",
				    "email": "taro@example.com"
				}
				""";

		mockMvc.perform(post("/demo/add").contentType("application/json").content(json)).andExpect(status().isOk())
				.andExpect(content().string("Saved"));
	}

	@Test
	void ユーザー一覧取得() throws Exception {
		UserResponse user = new UserResponse();
		user.setName("taro");
		List<UserResponse> mock = List.of(user);

		when(userService.getAllUsers()).thenReturn(mock);

		mockMvc.perform(get("/demo/all")).andExpect(status().isOk()).andExpect(jsonPath("$[0].name").value("taro"));
	}
}

サービスのユニットテストの追加

サービスでは Repository をモックします
これも同様に Repository が返却時に期待する値をモックにセットしサービス内のメソッドをコールすることでテストします

verify はメソッドがコールされたかの振る舞いをテストするためのアサーションです

  • vim src/test/java/com/example/demo/service/UserServiceTest.java
package com.example.demo.service;

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 static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;

import java.util.List;

import com.example.demo.dto.UserRequest;
import com.example.demo.entity.User;
import com.example.demo.repository.UserRepository;

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

	@Mock
	private UserRepository userRepository;

	@InjectMocks
	private UserService userService;

	@Test
	void ユーザー作成できる() {
		UserRequest req = new UserRequest();
		req.setName("taro");
		req.setEmail("taro@example.com");

		userService.createUser(req);

		verify(userRepository).save(any(User.class));
	}

	@Test
	void findAllメソッドが呼ばれたか() {
		userService.getAllUsers();

		verify(userRepository).findAll();
	}

	@Test
	void ユーザーの一覧が取得できる() {
		User user = new User();
		user.setName("taro");
		user.setEmail("taro@example.com");

		when(userRepository.findAll()).thenReturn(List.of(user));

		var result = userService.getAllUsers();

		assertEquals(1, result.size());
		assertEquals("taro", result.get(0).getName());
	}

	@Test
	void メールが重複している場合はエラー() {
		UserRequest req = new UserRequest();
		req.setName("taro");
		req.setEmail("taro@example.com");

		// モックの振る舞いを定義
		when(userRepository.existsByEmail("taro@example.com")).thenReturn(true);

		// 例外が投げられることを検証
		IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> userService.createUser(req));

		assertEquals("既に登録されています", ex.getMessage());

		// saveが呼ばれていないことも重要
		verify(userRepository, never()).save(any());
	}
}

リポジトリのインテグレーションテストの追加

実際に MySQL コンテナに接続しテストします
MySQL を起動するのは org.testcontainers.mysql.MySQLContainer を使います

接続先情報やデータベースの作成はテスト時に自動で行ってくれます
コンテナを起動したりマイグレーションするので少しテストに時間がかかります

  • vim src/test/java/com/example/demo/repository/UserRepositoryIntegrationTest.java
package com.example.demo.repository;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import static org.assertj.core.api.Assertions.assertThat;

import com.example.demo.entity.User;

@Import(TestcontainersConfiguration.class)
@SpringBootTest
class UserRepositoryIntegrationTest {

	@Autowired
	private UserRepository userRepository;

	@Test
	void DBに保存できる() {
		User user = new User();
		user.setName("taro");
		user.setEmail("taro@example.com");

		userRepository.save(user);

		Iterable<User> users = userRepository.findAll();
		assertThat(users).hasSize(1);
	}
}

MySQL コンテナを起動するテストの設定は以下で行います

  • vim src/test/java/com/example/demo/repository/TestcontainersConfiguration.java
package com.example.demo.repository;

import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.testcontainers.mysql.MySQLContainer;
import org.testcontainers.utility.DockerImageName;

@TestConfiguration(proxyBeanMethods = false)
class TestcontainersConfiguration {

	@Bean
	@ServiceConnection
	MySQLContainer mysqlContainer() {
		return new MySQLContainer(DockerImageName.parse("mysql:latest"));
	}

}

動作確認

  • JASYPT_PASSWORD="xxx" ./gradlew test

JASYPT_PASSWORD は jasypt で application.properties を暗号化している場合に指定してください
テストが完了すれば OK です
2回目以降はキャッシュを参照してしまいテストが速攻で終了してしまうのでその場合は

  • JASYPT_PASSWORD="xxx" ./gradlew cleanTest test

最後に

SpringBoot でユニットテストとインテグレーションテストを作成する方法を紹介しました
ある程度アプリが大きくなる前に全体を通してテストするケースは作成しておきましょう

値の検証や境界値などテストパターンが不十分なのでその辺りはいろいろ追加してください

また異常系のテストも不足しているので必要に応じて追加してください

0 件のコメント:

コメントを投稿