내 CI/CD 파이프라인은 3개월 동안 통과되었다 — 그 후 로그를 읽었을 때
요약
CI/CD 파이프라인이 모든 테스트를 통과했음에도 실제 기능이 작동하지 않았던 사례를 통해 과도한 모킹(Over-mocking)의 위험성을 경고합니다. 통합 테스트가 모의 객체(Mock)에 의존할 때 발생하는 검증 공백과 그 해결책을 다룹니다.
핵심 포인트
- 과도한 모킹은 통합 테스트를 무의미한 단위 테스트로 전락시킴
- 통합 테스트는 실제 서비스 경계(DB, API 등)를 포함해야 함
- 모킹은 단위 테스트 수준에서만 제한적으로 사용 권장
- 모킹과 실제 데이터 간의 불일치를 방지하기 위해 계약 테스트 도입 필요
내 CI/CD 파이프라인은 3개월 동안 통과되었다 — 그 후 로그를 읽었을 때
초록색 체크표시는 우리가 가장 좋아하는 색이었습니다. 모든 PR(Pull Request). 모든 merge(병합). 모든 deploy(배포). 모두 초록색이었습니다.
그러던 어느 화요일 오후, 한 사용자가 몇 주 동안이나 고장 나 있었던 기능에 대해 보고했습니다. 몇 시간이 아니라, 몇 주 동안 말이죠.
나는 CI 파이프라인 로그를 열었습니다. 그리고 그때 깨달았습니다 — 우리의 "통과"된 빌드들이 내내 우리를 속이고 있었다는 사실을 말입니다.
설정 (The Setup)
우리 파이프라인은 교과서처럼 보였습니다:
- Lint → 2. Unit tests (단위 테스트) → 3. Integration tests (통합 테스트) → 4. Build (빌드) → 5. Deploy (배포)
모든 단계가 통과되었습니다. 100% 성공률. 수개월 동안 말이죠.
발견 (The Discovery)
한 사용자가 버그를 신고했습니다: "내보내기(export) 버튼이 작동하지 않습니다." 내가 직접 클릭해 보았습니다 — 아무 일도 일어나지 않았습니다. 에러 메시지도, 충돌(crash)도 없었습니다. 그저... 침묵뿐이었습니다.
나는 이를 11주 전의 PR로 추적했습니다. 파이프라인은 통과되었습니다. 코드 리뷰(code review)도 승인되었습니다. 하지만 그 기능은 첫날부터 조용히 고장 나 있었습니다.
무엇이 잘못되었는지 알려드리겠습니다 — 그리고 그것은 여러분이 예상하는 것과는 다릅니다.
문제: 테스트하지 않는 테스트 (The Problem: Tests That Don't Test)
우리의 integration test(통합 테스트) 스위트에 버그가 있었습니다. 테스트 코드 자체에 실제 버그가 있었던 것입니다.
// 우리가 테스트하고 있다고 생각한 것:
describe('Export feature', () => {
it('should export user data', async () => {
...
Mock(모의 객체)이 모듈 수준에서 설정되어 있었습니다. export 모듈을 가져오는(import) 모든 테스트는 모킹된 버전을 가져갔습니다 — 바로 이런 종류의 버그를 잡아내기로 되어 있었던 integration tests(통합 테스트)를 포함해서 말이죠.
근본 원인: 과도한 모킹 (The Root Cause: Over-Mocking)
우리는 전형적인 over-mocking(과도한 모킹) 함정에 빠져 있었습니다:
- Unit tests (단위 테스트): 단위를 격리하기 위해 모든 것을 모킹함 → 괜찮음
- Integration tests (통합 테스트): "속도"를 위해 역시 모든 것을 모킹함 → 괜찮지 않음
- E2E tests (종단 간 테스트): 이 특정 흐름을 커버하지 못함 → 커버리지(coverage)의 공백
우리의 integration tests는 사실상 비용만 많이 드는 unit tests였습니다. 그것들은 우리의 실제 코드가 작동하는지를 검증한 것이 아니라, 우리의 mocks(모의 객체)가 올바르게 작동하는지를 검증했을 뿐이었습니다.
해결책 (The Fix)
실질적인 차이를 만들어낸 세 가지 변화입니다:
1. Mock Boundaries (모킹 경계)
단위 테스트 (Unit test) 수준에서만 모킹 (Mock) 하세요. 통합 테스트 (Integration tests)는 반드시 데이터베이스, API, 파일 시스템과 같은 실제 서비스 경계 (Service boundaries)를 거쳐야 합니다. 만약 테스트가 느리다면, 그것은 숨겨야 할 문제가 아니라 하나의 신호 (Signal)입니다.
2. 계약 테스트 (Contract Tests)
서비스 간에 계약 테스트 (Contract tests)를 추가했습니다. 만약 모킹 (Mock)이 실제 서비스가 반환하지 않을 값을 반환한다면, 계약 테스트가 이를 잡아냅니다.
// 계약 테스트: 모킹이 실제 동작과 일치하는지 확인
it('export mock matches real service contract', async () => {
const mockResult = await mockExport(userId);
...
3. 파이프라인 상태 지표 (Pipeline Health Metrics)
단순히 통과/실패 (Pass/fail) 비율뿐만 아니라, 테스트가 실제로 무엇을 실행했는지 추적하기 시작했습니다. 커버리지 (Coverage) 수치는 초기에 감소했습니다 (실제 커버리지 기준 94%에서 67%로). 그리고 그것은 우리가 몇 달 만에 얻은 가장 정직한 지표였습니다.
진짜 교훈 (The Real Lesson)
파이프라인이 초록색 (Green)이라고 해서 코드가 작동한다는 뜻은 아닙니다. 그것은 단지 테스트가 통과했다는 뜻입니다. 이 둘은 서로 다른 것입니다.
가장 위험한 버그는 파이프라인을 깨뜨리는 버그가 아닙니다. 파이프라인이 '괜찮다'라고 말하는 버그입니다.
만약 당신의 CI/CD가 항상 초록색이라면, 스스로에게 물어보세요. 내 테스트가 버그를 잡아내고 있는가, 아니면 그저 내 모킹 (Mocks)이 일관적이라는 것을 확인하고 있는 것뿐인가?
이제 내가 스스로에게 던지는 질문들
- 실제 버그 때문에 테스트가 _실패 (Failed)_했던 마지막은 언제인가?
- 나의 통합 테스트 (Integration tests)는 실제로 통합을 수행하고 있는가?
- 만약 모든 모킹 (Mocks)을 제거한다면, 얼마나 많은 테스트가 여전히 통과할 것인가?
- 나는 테스트 커버리지 (Test coverage)를 측정하고 있는가, 아니면 테스트 신뢰도 (Test confidence)를 측정하고 있는가?
절대 실패하지 않는 파이프라인은 신뢰할 수 있는 것이 아닙니다. 그것은 테스트되지 않은 것입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기