3개월 동안 통과된 내 CI/CD 파이프라인 — 그러다 로그를 읽게 되었다
초록색 체크 표시를 보면 기분이 좋다. 모든 pull request가 통과되었고, 모든 배포가 성공했다.
그러다 한 사용자가 기능 오류를 보고했다. 이미 몇 주 동안이나 오류가 발생하고 있었던 것이다.
파이프라인 로그를 열어보았다. 성공적으로 통과된 빌드들이 우리를 속이고 있었다.
우리의 프로세스는 완벽해 보였다:
- Linting
- Unit tests
- Integration tests
- Build
- Deploy
몇 달 동안 모든 단계의 성공률은 100%였다.
내보내기(export) 버튼을 클릭해도 아무런 반응이 없었다. 에러도 뜨지 않았다. 원인을 추적해 보니 11주 전의 코드 변경 사항이 문제였다. 파이프라인은 통과했고, 코드 리뷰도 승인되었다. 기능은 처음부터 제대로 작동하지 않았던 것이다.
문제는 코드가 아니었다. 바로 테스트 코드였다.
통합 테스트에서 모든 것을 mock으로 처리하고 있었다. export 서비스 전체를 mock으로 만들었다. 테스트는 실제 코드가 아닌 mock을 검증하고 있었다. mock은 언제나 성공 상태를 반환했다.
우리는 '과도한 mock(over-mocking)'의 함정에 빠져 있었다:
- Unit tests: 단위를 격리하기 위해 모든 것을 mock 처리함. 이는 괜찮다.
- Integration tests: 속도를 위해 모든 것을 mock 처리함. 이는 실수다.
- E2E tests: 이 특정 흐름을 놓침.
우리의 통합 테스트는 그저 비용만 많이 드는 단위 테스트에 불과했다. mock이 잘 작동하는지는 확인했지만, 실제 코드가 제대로 작동하는지는 확인하지 못했다.
이를 해결하기 위해 세 가지 변화를 주었다:
mock 사용을 unit test로 제한한다. Integration test는 반드시 실제 데이터베이스, API, 파일 시스템과 연결되어야 한다. 테스트가 느리다면 mock으로 숨기지 마라. 그 느린 속도를 최적화가 필요하다는 신호로 받아들여야 한다.
contract test를 추가한다. 이는 mock이 실제 서비스의 동작과 일치하는지 보장한다. 만약 mock이 실제 서비스라면 반환하지 않았을 데이터를 반환할 경우, contract test가 실패하게 된다.
실제 커버리지를 추적한다. 단순히 통과율만 확인하는 것을 그만두었다. 대신 테스트가 실제로 무엇을 실행하는지 확인했다. 그 결과 커버리지 수치는 94%에서 67%로 떨어졌다. 이것이 우리가 가진 가장 정직한 지표였다.
파이프라인이 초록색이라고 해서 코드가 제대로 작동한다는 뜻은 아니다. 그저 테스트가 통과했다는 뜻일 뿐이다. 이 둘은 엄연히 다르다.
가장 위험한 버그는 파이프라인이 '정상'이라고 말하는 버그다.
스스로에게 다음 질문들을 던져보라:
- 내 테스트는 버그를 찾아내고 있는가, 아니면 단순히 mock을 확인하고 있는가?
- 내 통합 테스트는 실제로 '통합'을 수행하고 있는가?
- 모든 mock을 제거한다면, 과연 몇 개의 테스트가 통과할 것인가?
- 나는 커버리지를 측정하고 있는가, 아니면 신뢰도를 측정하고 있는가?
절대 실패하지 않는 파이프라인은 신뢰할 수 없다. 그것은 테스트되지 않은 것이다.
Source: https://dev.to/kollittle/my-cicd-pipeline-passed-for-3-months-then-i-read-the-logs-4mbj
