概要
前回 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 件のコメント:
コメントを投稿