spring-boot-test について
Spring Boot アプリケーションのテストに取り組む開発者向けのリファレンススキルです。アプリケーションコンテキスト、Web、データ、シリアライズのテスト方法と、設定やよくある問題を説明します。
主な機能
- 01@SpringBootTest による全コンテキストテスト
- 02Web・データ層に絞ったスライステスト
- 03MockMvc によるリクエストと認証のテスト
- 04@MockBean と @SpyBean による Bean のモック化
- 05テストプロパティとカスタム設定
- 06AssertJ アサーションと問題の診断
活用シーン
- 01アプリケーション全体を起動せずコントローラーをテスト
- 02スライステストで JPA リポジトリを検証
- 03外部依存をモックに置き換える
- 04遅いテストや設定ミスを調査
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ claude-dev-suite/ claude-dev-suite --skill spring-boot-testインストール後、Claude Code で/spring-boot-testを入力して読み込まれたか確認します。
同梱ファイルquick-ref/annotations.md
詳細
Spring Boot のテスト
深い知識:包括的なドキュメントについては、technology:
spring-boot-testを指定してmcp__documentation__fetch_docsを使用してください。
このスキルを使用しない場合
- 純粋な単体テスト - Spring コンテキストを使わずに高速なテストを実行するには、Mockito とともに
junitを使用してください - 実際のデータベースを使う統合テスト - Testcontainers とともに
spring-boot-integrationを使用してください - REST API クライアントのテスト - HTTP テストには
rest-assuredを使用してください - E2E Web テスト - Selenium または Playwright を使用してください
- マイクロサービスのコントラクトテスト - Spring Cloud Contract を使用してください
依存関係
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
含まれるもの:JUnit 5、Mockito、AssertJ、Hamcrest、JSONPath、Spring Test
テストアノテーション
@SpringBootTest - 全コンテキスト
@SpringBootTest
class ApplicationTest {
@Autowired
private UserService userService;
@Test
void contextLoads() {
assertThat(userService).isNotNull();
}
}
// With web environment
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class WebApplicationTest {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@Test
void healthCheck() {
ResponseEntity<String> response = restTemplate
.getForEntity("/actuator/health", String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
}
スライステスト(高速で対象を絞ったテスト)
| アノテーション | レイヤー | 自動構成 |
|---|---|---|
@WebMvcTest | コントローラー | MockMvc、Jackson |
@DataJpaTest | JPA リポジトリ | TestEntityManager、DataSource |
@DataMongoTest | MongoDB | MongoTemplate |
@JsonTest | JSON シリアライゼーション | JacksonTester |
@RestClientTest | REST クライアント | MockRestServiceServer |
// Controller test - only loads web layer
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
void getUser_ReturnsUser() throws Exception {
when(userService.findById(1L))
.thenReturn(Optional.of(new User(1L, "John")));
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("John"));
}
}
// Repository test - uses embedded database
@DataJpaTest
class UserRepositoryTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private UserRepository repository;
@Test
void findByEmail_ReturnsUser() {
User user = new User("[email protected]", "Test User");
entityManager.persistAndFlush(user);
Optional<User> found = repository.findByEmail("[email protected]");
assertThat(found).isPresent()
.hasValueSatisfying(u -> assertThat(u.getName()).isEqualTo("Test User"));
}
}
MockMvc
基本的なリクエスト
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
// GET request
@Test
void getUsers() throws Exception {
when(userService.findAll()).thenReturn(List.of(
new User(1L, "Alice"),
new User(2L, "Bob")
));
mockMvc.perform(get("/api/users")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$", hasSize(2)))
.andExpect(jsonPath("$[0].name").value("Alice"));
}
// POST request with JSON body
@Test
void createUser() throws Exception {
CreateUserRequest request = new CreateUserRequest("John", "[email protected]");
User createdUser = new User(1L, "John", "[email protected]");
when(userService.create(any())).thenReturn(createdUser);
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"name": "John",
"email": "[email protected]"
}
"""))
.andExpect(status().isCreated())
.andExpect(header().exists("Location"))
.andExpect(jsonPath("$.id").value(1));
}
// PUT request
@Test
void updateUser() throws Exception {
mockMvc.perform(put("/api/users/{id}", 1)
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"name": "Updated Name"}
"""))
.andExpect(status().isOk());
verify(userService).update(eq(1L), any());
}
// DELETE request
@Test
void deleteUser() throws Exception {
mockMvc.perform(delete("/api/users/{id}", 1))
.andExpect(status().isNoContent());
verify(userService).delete(1L);
}
}
認証を使用する場合
@WebMvcTest(AdminController.class)
@Import(SecurityConfig.class)
class AdminControllerTest {
@Autowired
private MockMvc mockMvc;
// Using @WithMockUser
@Test
@WithMockUser(roles = "ADMIN")
void adminEndpoint_WithAdminRole_Succeeds() throws Exception {
mockMvc.perform(get("/api/admin/dashboard"))
.andExpect(status().isOk());
}
@Test
@WithMockUser(roles = "USER")
void adminEndpoint_WithUserRole_Forbidden() throws Exception {
mockMvc.perform(get("/api/admin/dashboard"))
.andExpect(status().isForbidden());
}
// Using JWT token
@Test
void withJwtToken() throws Exception {
String token = jwtTokenProvider.createToken("admin", List.of("ROLE_ADMIN"));
mockMvc.perform(get("/api/admin/dashboard")
.header("Authorization", "Bearer " + token))
.andExpect(status().isOk());
}
}
モック化
@MockBean
@SpringBootTest
class OrderServiceTest {
@Autowired
private OrderService orderService;
@MockBean // Replaces bean in context with mock
private PaymentGateway paymentGateway;
@MockBean
private InventoryService inventoryService;
@Test
void placeOrder_WhenPaymentSucceeds_CreatesOrder() {
when(inventoryService.checkStock(any())).thenReturn(true);
when(paymentGateway.charge(any())).thenReturn(PaymentResult.success());
Order order = orderService.placeOrder(new OrderRequest(...));
assertThat(order.getStatus()).isEqualTo(OrderStatus.CONFIRMED);
verify(paymentGateway).charge(any());
}
@Test
void placeOrder_WhenPaymentFails_ThrowsException() {
when(paymentGateway.charge(any()))
.thenThrow(new PaymentException("Card declined"));
assertThatThrownBy(() -> orderService.placeOrder(new OrderRequest(...)))
.isInstanceOf(PaymentException.class)
.hasMessage("Card declined");
}
}
@SpyBean
@SpringBootTest
class NotificationServiceTest {
@Autowired
private NotificationService notificationService;
@SpyBean // Wraps real bean, allows partial mocking
private EmailSender emailSender;
@Test
void sendNotification_CallsEmailSender() {
notificationService.notify(user, "Hello");
verify(emailSender).send(eq(user.getEmail()), any());
}
@Test
void sendNotification_WhenEmailFails_LogsError() {
doThrow(new EmailException("SMTP error"))
.when(emailSender).send(any(), any());
// Method should handle exception gracefully
assertThatCode(() -> notificationService.notify(user, "Hello"))
.doesNotThrowAnyException();
}
}
テスト構成
テストプロパティ
@SpringBootTest
@TestPropertySource(properties = {
"app.feature.enabled=true",
"app.external.url=http://localhost:8080"
})
class FeatureTest { }
// Or use test profile
@SpringBootTest
@ActiveProfiles("test")
class ProfileTest { }
application-test.yml
spring:
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: create-drop
app:
external:
url: http://localhost:${wiremock.server.port}
カスタムテスト構成
@TestConfiguration
public class TestConfig {
@Bean
@Primary
public Clock testClock() {
return Clock.fixed(
Instant.parse("2025-01-15T10:00:00Z"),
ZoneId.of("UTC")
);
}
@Bean
@Primary
public PaymentGateway testPaymentGateway() {
return new FakePaymentGateway();
}
}
@SpringBootTest
@Import(TestConfig.class)
class TimeBasedFeatureTest { }
AssertJ アサーション
// Basic assertions
assertThat(user.getName()).isEqualTo("John");
assertThat(user.getAge()).isGreaterThan(18);
assertThat(user.getEmail()).contains("@").endsWith(".com");
// Collection assertions
assertThat(users)
.hasSize(3)
.extracting(User::getName)
.containsExactly("Alice", "Bob", "Charlie");
// Exception assertions
assertThatThrownBy(() -> service.process(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("null");
// Optional assertions
assertThat(repository.findById(1L))
.isPresent()
.hasValueSatisfying(user ->
assertThat(user.getName()).isEqualTo("John")
);
// Soft assertions (collect all failures)
SoftAssertions.assertSoftly(softly -> {
softly.assertThat(user.getName()).isEqualTo("John");
softly.assertThat(user.getEmail()).contains("@");
softly.assertThat(user.getAge()).isPositive();
});
テストデータビルダー
public class UserTestBuilder {
private Long id = 1L;
private String name = "Test User";
private String email = "[email protected]";
private UserRole role = UserRole.USER;
public static UserTestBuilder aUser() {
return new UserTestBuilder();
}
public UserTestBuilder withId(Long id) {
this.id = id;
return this;
}
public UserTestBuilder withName(String name) {
this.name = name;
return this;
}
public UserTestBuilder withEmail(String email) {
this.email = email;
return this;
}
public UserTestBuilder withRole(UserRole role) {
this.role = role;
return this;
}
public UserTestBuilder asAdmin() {
this.role = UserRole.ADMIN;
return this;
}
public User build() {
return new User(id, name, email, role);
}
}
// Usage
User admin = aUser().withName("Admin").asAdmin().build();
User regularUser = aUser().build();
アンチパターン
| アンチパターン | 問題点 | 解決策 |
|---|---|---|
| すべてのテストで @SpringBootTest を使用する | 非常に遅くなります | スライステスト(@WebMvcTest、@DataJpaTest)を使用する |
| @MockBean を使用しない | 実際の Bean をテストします | 外部依存関係をモック化する |
| テストでポートをハードコードする | ポートが競合します | RANDOM_PORT とともに @LocalServerPort を使用する |
| private メソッドをテストする | 実装に強く結びつきます | コントローラー/サービス API 経由でテストする |
| テストデータを分離しない | テスト同士が干渉します | @Transactional を使用するか、@AfterEach でクリーンアップする |
| @Sql スクリプトを使用しない | 手動セットアップが重複します | テストデータのセットアップに @Sql を使用する |
| テストプロファイルがない | 開発/本番環境の設定を汚染します | @ActiveProfiles("test") を使用する |
問題の迅速な解決
| 問題 | 考えられる原因 | 解決策 |
|---|---|---|
| "Unable to find @SpringBootConfiguration" | メインクラスが見つかりません | @SpringBootTest(classes = App.class) を追加する |
| テストが非常に遅い | 不必要に @SpringBootTest を使用しています | スライステスト(@WebMvcTest など)を使用する |
| "No qualifying bean" | @MockBean がありません | 依存関係に @MockBean を追加する |
| ポートがすでに使用されています | ポートがハードコードされています | webEnvironment = RANDOM_PORT を使用する |
| "Could not autowire" | Bean がテストコンテキストにありません | コンポーネントスキャンを確認するか、@Import を追加する |
| 不安定なテスト | データベースの状態がリセットされていません | @Transactional または @DirtiesContext を使用する |
参考資料
- クイックリファレンス:アノテーション
- Spring Boot テストガイド
- 関連項目:Testcontainers については
spring-boot-integration
コメント
Markdown 対応