Develop

[Spring Boot] 게시판 - 07. 통합 테스트 작성하기 (MockMvc & Fixture 패턴) 본문

백엔드/게시판 만들기

[Spring Boot] 게시판 - 07. 통합 테스트 작성하기 (MockMvc & Fixture 패턴)

230801 2025. 11. 27. 05:12

안녕하세요 .ᐟ

오늘은 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 스펙이 일치하는지 다시한번 점검하기 .ᐟ