Skip to content

Issue #17: [Quality] 통합 테스트 작성 #22

@al1kite

Description

@al1kite

Labels: testing, quality, high-priority
Milestone: Quality & Documentation
Assignee: @al1kite

목적

전체 플로우를 E2E로 검증

테스트 대상

1. 정상 플로우

@SpringBootTest(webEnvironment = RANDOM_PORT)
@AutoConfigureMockMvc
class AnalysisIntegrationTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @Test
    void fullFlow_success() throws Exception {
        // 1. 파일 업로드 (분석 요청)
        var file = new MockMultipartFile(
            "file",
            "test.csv",
            "text/csv",
            createValidCsvContent().getBytes()
        );
        
        var result = mockMvc.perform(
                multipart("/api/analysis")
                    .file(file))
            .andExpect(status().isCreated())  // 201
            .andExpect(header().exists("Location"))
            .andExpect(jsonPath("$.analysisId").exists())
            .andReturn();
        
        var response = objectMapper.readValue(
            result.getResponse().getContentAsString(),
            AnalysisIdResponse.class
        );
        
        var analysisId = response.analysisId();
        
        // 2. 결과 조회
        mockMvc.perform(get("/api/analysis/" + analysisId))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.analysisId").value(analysisId))
            .andExpect(jsonPath("$.basicStats.totalRequests").value(1000))
            .andExpect(jsonPath("$.topStats.topIps").isArray())
            .andExpect(jsonPath("$.ipDetails").isNotEmpty());
        
        // 3. 전체 목록 조회
        mockMvc.perform(get("/api/analysis"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$").isArray())
            .andExpect(jsonPath("$[0].analysisId").value(analysisId));
        
        // 4. 삭제
        mockMvc.perform(delete("/api/analysis/" + analysisId))
            .andExpect(status().isNoContent());  // 204
        
        // 5. 삭제 확인
        mockMvc.perform(get("/api/analysis/" + analysisId))
            .andExpect(status().isNotFound());  // 404
    }
}

2. 에러 케이스

@Test
void invalidFile_returns400() throws Exception {
    var file = new MockMultipartFile(
        "file",
        "test.txt",
        "text/plain",
        "not a csv".getBytes()
    );
    
    mockMvc.perform(multipart("/api/analysis").file(file))
        .andExpect(status().isBadRequest())
        .andExpect(jsonPath("$.errorCode").value("INVALID_FILE"))
        .andExpect(jsonPath("$.errorId").exists());
}

@Test
void fileTooLarge_returns413() throws Exception {
    var largeContent = new byte[60 * 1024 * 1024];  // 60MB
    var file = new MockMultipartFile("file", "large.csv", "text/csv", largeContent);
    
    mockMvc.perform(multipart("/api/analysis").file(file))
        .andExpect(status().isPayloadTooLarge())
        .andExpect(jsonPath("$.errorCode").value("FILE_TOO_LARGE"))
        .andExpect(jsonPath("$.details.fileSizeMB").value(60));
}

@Test
void rateLimit_returns429() throws Exception {
    var file = createValidFile();
    
    // 10번 성공
    for (int i = 0; i < 10; i++) {
        mockMvc.perform(multipart("/api/analysis").file(file))
            .andExpect(status().isCreated());
    }
    
    // 11번째 실패
    mockMvc.perform(multipart("/api/analysis").file(file))
        .andExpect(status().isTooManyRequests())
        .andExpect(header().exists("Retry-After"));
}

3. 캐싱 테스트

@Test
void getResult_withETag_returns304() throws Exception {
    var analysisId = createAnalysis();
    
    // 첫 요청
    var firstResult = mockMvc.perform(get("/api/analysis/" + analysisId))
        .andExpect(status().isOk())
        .andExpect(header().exists("ETag"))
        .andReturn();
    
    var etag = firstResult.getResponse().getHeader("ETag");
    
    // If-None-Match로 재요청
    mockMvc.perform(get("/api/analysis/" + analysisId)
            .header("If-None-Match", etag))
        .andExpect(status().isNotModified())  // 304
        .andExpect(header().exists("ETag"))
        .andExpect(content().string(""));  // body 없음
}

4. 동시성 테스트

@Test
void concurrentRequests_allSucceed() throws Exception {
    var executor = Executors.newFixedThreadPool(10);
    var files = IntStream.range(0, 20)
        .mapToObj(i -> createValidFile())
        .toList();
    
    var futures = files.stream()
        .map(file -> CompletableFuture.runAsync(() -> {
            try {
                mockMvc.perform(multipart("/api/analysis").file(file))
                    .andExpect(status().isCreated());
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }, executor))
        .toList();
    
    CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
        .get(10, TimeUnit.SECONDS);
    
    // 모두 성공
    assertThat(futures).allMatch(f -> !f.isCompletedExceptionally());
}

작업 내용

  • 정상 플로우 통합 테스트
  • 에러 케이스 테스트 (400, 404, 413, 429, 500)
  • 캐싱 테스트 (ETag, 304)
  • 동시성 테스트
  • 성능 테스트 (대용량 파일)
  • 테스트 커버리지 80% 이상

의존성

testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.testcontainers:testcontainers:1.19.0")  // 선택

완료 조건

  • 모든 API 엔드포인트 테스트
  • 모든 에러 케이스 커버

Metadata

Metadata

Assignees

Labels

documentationImprovements or additions to documentation

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions