| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
| 29 | 30 | 31 |
- mysql 표 출력
- 이진탐색
- cs
- 분할정복 방법
- 티스토리챌린지
- 동적 프로그래밍 방법
- table status
- VI
- spring boot
- 오블완
- mycli
- Less
- Pager
- 오일러 경로
- 인프런워밍업클럽
- 욕심쟁이 방법
- 맥
- MySQL
- oh-my-zsh
- 데이크스트라
- 스터디2기
- 인프런
- zsh theme
- 순차탐색
- 터미널
- zsh
- 네트워킹데이
- mysql 표
- 알고리즘
- CS스터디
- Today
- Total
Develop
[Spring Boot] 게시판 - 07. 통합 테스트 작성하기 (MockMvc & Fixture 패턴) 본문
안녕하세요 .ᐟ
오늘은 Spring Boot 프로젝트에서 통합 테스트를 작성하는 방법을 정리합니다.
어제 작성한 단위 테스트와 달리, 통합 테스트는 실제 Spring 컨테이너를 띄워서 여러 레이어가 함께 잘 동작하는지 검증하는 테스트입니다. MockMvc를 활용해 HTTP 요청을 시뮬레이션하고, Fixture 패턴으로 테스트 데이터를 관리하는 방법을 배웠습니다~
단위 테스트 vs 통합 테스트
| 범위 | 개별 메서드 (Service 레이어) | 여러 컴포넌트 (Controller → Service → Repository) |
| Spring 컨테이너 | ❌ 사용 안함 | ✅ 실제로 띄움 |
| DB | Mock 객체 | 실제 DB (H2) |
| 속도 | 빠름 (0.1초) | 느림 (2-3초) |
| 목적 | 비즈니스 로직 검증 | API 전체 플로우 검증 |
테스트 환경 구성
1. 의존성 추가
dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'com.h2database:h2' // 테스트용 인메모리 DB
}
2. 테스트 전용 설정 파일
application-test.yml
# spring
spring:
datasource:
url: jdbc:h2:mem:testdb;MODE=MySQL
driver-class-name: org.h2.Driver
username: sa
password:
jpa:
hibernate:
ddl-auto: create-drop # 테스트 시작시 생성, 종료시 삭제
properties:
hibernate:
dialect: org.hibernate.dialect.H2Dialect
show_sql: false # 콘솔 출력 최소화
format_sql: true
h2:
console:
enabled: true
# logging - 에러만 출력
logging:
level:
root: error
org.springframework: error
org.hibernate: error
exception-conversion-word: '%wEx{short}' # 스택 트레이스 짧게
3. Gradle 테스트 설정
build.gradle
test {
useJUnitPlatform()
testLogging {
events "failed" // 실패한 것만 출력
exceptionFormat "short" // 스택 트레이스 짧게
showStackTraces true
showCauses true
showExceptions true
}
}
사용되는 어노테이션들
@SpringBootTest // 전체 Spring 컨테이너를 띄움
@AutoConfigureMockMvc // MockMvc 자동 설정
@ActiveProfiles("test") // application-test.yml 활성화
@Transactional // 각 테스트 후 자동 롤백
class PostIntegrationTest {
@Autowired
private MockMvc mockMvc; // HTTP 요청 시뮬레이션
@Autowired
private ObjectMapper objectMapper; // JSON 변환
}
@SpringBootTest
- 전체 Spring 애플리케이션 컨텍스트를 로드합니다. 실제 운영 환경과 동일하게 Bean들이 등록됩니다.
@AutoConfigureMockMvc
- MockMvc를 자동으로 설정해줍니다. 실제 서버를 띄우지 않고도 HTTP 요청을 테스트할 수 있습니다.
MockMvc란?
- 실제 HTTP 서버를 띄우지 않고, Spring MVC의 동작을 테스트할 수 있는 도구입니다.
- `mockMvc.perform()`으로 가상의 HTTP 요청을 보낼 수 있습니다.
ObjectMapper란?
- Java 객체와 JSON 문자열을 상호 변환하는 Jackson 라이브러리의 핵심 클래스입니다.
- 테스트에서 Request DTO를 JSON으로 변환할 때 사용합니다.
Fixture 패턴
- 테스트에서 반복적으로 사용하는 데이터를 생성하는 유틸리티 클래스입니다.
장점
- 유저, 게시글 정보 등 생성에 자주 사용되는 데이터를 한 곳에서 관리합니다.
- 엔티티가 수정되면 테스트 파일을 여기저기 바꿀 필요 없이 Fixture 파일만 수정하면 됩니다.
- 테스트 코드 중복을 제거하고 가독성을 높입니다.
UserFixture.java
public class UserFixture {
public static User createDefaultUser() {
return User.builder()
.name("테스트유저")
.email("test@test.com")
.password("encodedPassword")
.nickname("테스터")
.build();
}
public static User createUserWithName(String name) {
return User.builder()
.name(name)
.email(name + "@test.com")
.password("encodedPassword")
.nickname(name + "닉네임")
.build();
}
}
CategoryFixture.java
- 테스트 간 격리를 위해 categoryType별로 테스트를 작성했습니다.
public class CategoryFixture {
public static Category createDefaultCategory() {
return Category.builder()
.categoryType(CategoryType.FREE)
.description("자유게시판")
.isActive(true)
.build();
}
public static Category createQuestionCategory() {
return Category.builder()
.categoryType(CategoryType.QNA)
.description("질문게시판")
.isActive(true)
.build();
}
// CategoryType에 따라 자동으로 description 매핑
private static String getDescriptionByType(CategoryType type) {
return switch (type) {
case FREE -> "자유게시판";
case NOTICE -> "공지사항";
case QNA -> "질문게시판";
case TECH -> "기술게시판";
};
}
}
PostFixture.java
public class PostFixture {
public static Post createDefaultPost(User user, Category category) {
return Post.builder()
.title("테스트 게시글")
.content("테스트 내용입니다.")
.user(user)
.category(category)
.build();
}
}
통합 테스트 작성
1. 게시글 작성 테스트
- jsonPath 란?
- JSON 응답에서 특정 필드 값을 추출하는 표현식입니다.
@Test
@DisplayName("게시글 작성 성공")
void createPost_Integration() {
// given
User testUser = userRepository.save(UserFixture.createDefaultUser());
Category testCategory = categoryRepository.save(CategoryFixture.createDefaultCategory());
PostCreateRequest request = new PostCreateRequest(
"통합 테스트 게시글",
"통합 테스트 내용",
testCategory.getId()
);
// when & then
mockMvc.perform(post("/api/posts")
.param("userId", testUser.getId().toString())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated()) // 201 Created
.andExpect(jsonPath("$.title").value("통합 테스트 게시글"))
.andExpect(jsonPath("$.content").value("통합 테스트 내용"))
.andExpect(jsonPath("$.viewCount").value(0));
}
2. Validation 실패 테스트
- 일부러 잘못된 데이터를 내보내야 하므로 DTO 제약 없이 데이터를 구성할 수 있는 Map 을 사용했습니다.
@Test
@DisplayName("게시글 작성 실패 - 제목 길이 초과")
void createPost_Fail_TitleTooLong() {
// given
User testUser = userRepository.save(UserFixture.createDefaultUser());
String longTitle = "a".repeat(101); // 100자 초과
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("title", longTitle);
requestBody.put("content", "내용");
requestBody.put("categoryId", 1L);
String jsonRequest = objectMapper.writeValueAsString(requestBody);
// when & then
mockMvc.perform(post("/api/posts")
.param("userId", testUser.getId().toString())
.contentType(MediaType.APPLICATION_JSON)
.content(jsonRequest))
.andExpect(status().isBadRequest()); // 400 Bad Request
}
ObjectMapper를 사용하는 이유:
- 매우 긴 문자열(10,000자+)도 안전하게 JSON 변환
- String.format()보다 안정적
- 복잡한 중첩 객체도 처리 가능
3. 페이징 테스트
@Test
@DisplayName("게시글 목록 조회 - 페이징")
void getPosts_WithPaging() {
// given
User testUser = userRepository.save(UserFixture.createDefaultUser());
Category testCategory = categoryRepository.save(CategoryFixture.createDefaultCategory());
// 10개 게시글 생성
for (int i = 1; i <= 10; i++) {
Post post = Post.builder()
.title("게시글 " + i)
.content("내용 " + i)
.user(testUser)
.category(testCategory)
.build();
postRepository.save(post);
}
// when & then
mockMvc.perform(get("/api/posts")
.param("page", "0")
.param("size", "5"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.content").isArray())
.andExpect(jsonPath("$.content.length()").value(5))
.andExpect(jsonPath("$.totalElements").value(10))
.andExpect(jsonPath("$.totalPages").value(2));
}
테스트 결과
UserIntegrationTest ✅
- 회원가입 성공
- 회원가입 실패 (중복 체크)
- 전체 사용자 조회
- ID로 사용자 조회
- 사용자 정보 수정
- 사용자 삭제
PostIntegrationTest ✅
- 게시글 작성 성공
- 게시글 목록 조회 (페이징)
- 게시글 상세 조회
- 게시글 수정 성공
- 게시글 삭제 성공
- Validation 실패 케이스 (제목/내용 길이)
- 조회수 증가 검증
트러블 슈팅 - API 파라미터 불일치
MissingServletRequestParameterException:
Required request parameter 'userId' for method parameter type Long is not present
원인: Controller는 @RequestParam 인데 테스트는 @RequestHeader로 전달
// Controller
@PostMapping("/api/posts")
public ResponseEntity<?> create(
@RequestParam Long userId, // Query Parameter로 받음
@RequestBody @Valid PostCreateRequest request
) { ... }
// ❌ 잘못된 테스트
mockMvc.perform(post("/api/posts")
.header("User-Id", testUser.getId())) // Header로 전달
// ✅ 올바른 테스트
mockMvc.perform(post("/api/posts")
.param("userId", testUser.getId().toString())) // Query Parameter로 전달
@RequestParam vs @RequestHeader 차이
| 구분 | @RequestParam | @RequestHeader |
| 전달 방식 | Query String | HTTP Header |
| URL 예시 | /api/posts?userId=123 | Header: User-Id: 123 |
| MockMvc | .param("userId", "123") | .header("User-Id", "123") |
| 사용 | 검색조건, 페이징, 일반 파라미터 | JWT 토큰, 인증정보, API 버전 |
느낀 점
- 통합 테스트는 전체 플로우를 검증하기 때문에 단위 테스트보다 신뢰도가 높다.
- Fixture 패턴으로 테스트 데이터 관리가 훨씬 편해졌다.
- `@RequestParam` vs `@RequestHeader` 차이를 알게 되었고, 추후에 JWT 구현하면 `@AuthenticationPrincipal`로 개선할 예정이다.
- 테스트와 Controller의 API 스펙이 일치하는지 다시한번 점검하기 .ᐟ

'백엔드 > 게시판 만들기' 카테고리의 다른 글
| [Spring Boot] 게시판 - 09. 게시글 이미지 업로드 구현하기 (feat. 이미지 여러개 업로드 & 순서 관리) (0) | 2025.11.30 |
|---|---|
| [Spring Boot] 게시판 - 08. 파일 업로드 구현하기 (feat. 프로필 이미지 업로드) (0) | 2025.11.28 |
| [Spring Boot] 게시판 - 06. 단위 테스트 작성하기 (Mockito & AssertJ) (0) | 2025.11.24 |
| [Spring Boot] 게시판 - 05. Validation 검증 (입력값 유효성 검사) (1) | 2025.11.23 |
| [Spring Boot] 게시판 - 04. 전역 예외 처리 (Global Exception Handler) (0) | 2025.11.22 |