녹색 테스트가 내 파이프라인에 대해 거짓말을 했던 경험
요약
본 글은 단위 테스트(unit test)의 한계와 '녹색' 결과에 대한 신뢰도를 의심하게 된 경험을 공유합니다. 특히, 자식 실행이 생성만 되고 실제로 시작되지 않아 성공으로 위장한 교착 상태를 발견했습니다. 이는 실제 워커 환경에서만 노출되는 배관(plumbing) 레벨의 결함이었으며, 회귀 테스트 추가로 해결되었습니다.
핵심 포인트
- 단위 테스트는 경계선에서 멈추기 쉬우므로 전체 파이프라인을 검증하기 어렵다.
- 성공 코드(200)가 실제 동작 여부를 보장하지 않는 '교착 상태'의 위험성을 지적한다.
- 실제 워커 환경(live stack)에서의 테스트가 필수적이며, 회귀 테스트를 통해 결함을 발견했다.
- 파이프라인 오케스트레이션 설계와 자식 실행의 시작 과정을 명확히 분리해야 한다.
v0.2.0 필드 테스트의 시나리오 S7에는 '종단 간(end to end)'으로 실행되는 파이프라인이 있었습니다. 단위 테스트 스위트(unit suite)는 녹색(green)였습니다. 컨테이너 시나리오는 180초에서 시간 초과되었습니다.
해당 파이프라인은 모든 자식 실행(child run)을 생성하고, 각 ID를 반환했지만, 단 하나도 시작하지 않았습니다. 자식들은 영원히 queued 상태에 머물렀고, 부모는 성공했다고 보고했습니다. 제가 주장했던 모든 것 — 상태 코드, 지속된 레코드 — 은 사실이었습니다. 하지만 제가 신경 써야 할 동작은 전혀 일어나지 않았습니다.
그것이 바로 제가 200(성공)을 더 이상 신뢰하지 않게 된 순간이었습니다.
저는 HivePlane v0.2.0의 필드 테스트 한 번에서 이 결함 클래스를 네 번 발견했습니다. 모든 인스턴스는 동일한 형태였습니다: 결과(outcome)가 아닌 배관(plumbing)을 증명하는 테스트였습니다. 모든 테스트는 단위 테스트 스위트에게 보이지 않았는데, 왜냐하면 단위 테스트 스위트는 경계선에서 멈췄기 때문입니다.
그 형태
네 가지 발견. 하나의 형태. 결과가 아닌 배관을 증명하는 테스트.
| 발견 | 녹색 테스트가 증명한 것 | 실제로 사실이었던 것 |
|---|---|---|
| D-3, 파이프라인이 완료되지 않음 | 자식 실행이 존재함 (201, 지속됨) | queued → running으로 전환된 적 없음; 아무것도 시작하지 않았음 |
| ... | ||
| 단위 테스트는 경계선에서 멈췄습니다. 버그는 그보다 한 단계 더 깊은 곳에 살고 있었습니다. |
시작되지 않은 파이프라인
RunNodeExecutor.submit은 다음과 같이 작동했습니다:
run = self._runs.submit(workload=..., pipeline_origin=origin, ctx=ctx)
return run.id
그게 전부였습니다. 자식을 생성하고 반환하는 것만 했습니다. 실행(run)은 queued → running으로 전환될 때에만 어댑터로 디스패치됩니다. Docker 프로파일에는 대기 중인 실행을 임대할 워커 데몬이 없었고, 파이프라인 드라이버는 이미 running 상태인 노드만 조정했습니다. 따라서 부모는 아무것도 시작하지 않을 자식(child)을 기다리게 되었는데 — 이는 **성공으로 위장한 교착 상태(deadlock)**였습니다.
수정은 한 줄이었고, 원래 그곳에 있어야 했을 줄입니다:
run = self._runs.submit(...)
if run.state is RunState.QUEUED:
self._runs.start(run.id, actor=
회귀 테스트(regression test)인 `test_pipeline_starts_its_child_runs`는 자식 작업이 `running` 상태로 전환되는지 확인합니다. 이 테스트는 이전에는 실패했지만, 지금은 통과했습니다. 왜 제가 이걸 가지고 있지 않았을까요? 제 단위 테스트(unit test)가 동기식 실행기(synchronous executor)를 사용해서 누락된 시작 과정을 숨겼기 때문입니다. 동기식 경로는 워커(worker)가 필요하지 않으므로, 워커의 부재는 눈에 띄지 않았습니다. 오직 워커가 없는 실제 스택(live stack)만이 이를 노출했습니다. Docker 보고서의 [§3.3](https://github.com/deghosal-2026/hiveplane/blob/main/docs/field-test/v0.2.0/DOCKER_TEST_REPORT.md)는 정확한 전환 과정을 추적하며, [오케스트레이션 설계(orchestration design)](https://github.com/deghosal-2026/hiveplane/blob/main/docs/design/orchestration-design.md)가 파이프라인 실행 모델을 문서화하고 있습니다.
> **팁:** 자식 작업을 생성하는 모든 구성 요소(파이프라인, 트리거, 인증 등)는 해당 작업을 시작하거나, 해당 작업을 임대하는 워커에 의존해야 합니다. 테스트 실행기가 동기식이라면, 당신이 배포하려는 정확한 버그를 숨기고 있는 것입니다.
## 어둠 속에서 성공한 전달(delivery)
D-4가 더 교묘했습니다. 팬아웃(fan-out)은 실제로 작동했습니다. 웹훅 싱크(webhook sink)는 `run.completed` 페이로드를 기록했습니다. 시도들은 `fan_out_deliveries`에 `delivered` 상태로 작성되었습니다.
하지만 제가 시나리오에서 폴링했던 `/delivery/audit`는 M51의 `delivery_attempts` 스토어를 읽습니다. 두 개의 하위 시스템, 하나의 개념, 두 개의 스토어입니다. 성공적인 전달은 **제어 평면(control plane)을 통해서는 감사할 수 없었습니다**. 제 테스트는 200과 빈 리스트를 확인하고 넘어갔는데, 이는 빈 리스트가
이 부분이 저를 가장 무섭게 만들었습니다. 인터랙티브하고 모바일 환경에서의 승인(approvals) — Slack 버튼, 이메일 링크 등 — 은 `resolve()` 함수가 아무 동작도 하지 않는 (no-op) 람다에 연결되어 있었습니다:
배포된 코드
approval.resolve = lambda decision: None # 결정은 기록되었으나, 아무것도 건드리지 않음
결정(decision)은 `approval_decisions` 테이블에 영구적으로 저장되었습니다. 테스트는 해당 행이 존재하는지 확인했습니다. 테스트는 통과했습니다. 하지만 일시 중단된 실행(run)은 절대 재개되지 않았습니다. 운영자가 '승인'을 클릭하고, 확인 메시지를 보았지만, 실행은 영원히 일시 중단 상태로 머물렀습니다.
녹색으로 나온 테스트는 한 행이 작성되었다는 것만 증명했습니다. 그 실행(run)이 이동했다는 것은 증명하지 못했습니다. 해결책은 `resolve()`가 `RunService.resume(run_id)`를 호출하도록 하는 것이었습니다. 이것이야말로 운영자가 실제로 신경 쓰는 부수 효과(side effect)입니다. 회귀 테스트(regression test)는 결정 기록의 존재 여부가 아니라, 실행이 'running' 상태로 전환되는지 확인합니다.
> **팁:** 가장 위험한 '녹색' 테스트는 기록만 확인하고 실제 효과를 확인하지 않는 테스트입니다. 승인 과정에서 아무런 재개 없이 단순히 기록만 남기는 것은 워크플로우처럼 위장된 단순 동작(no-op)일 뿐입니다. 테스트에게 물어보세요: '운영자가 무엇을 보았는가?' 그리고 그것을 검증하세요.
## 제가 배운 것들
단위 테스트(unit test)는 경계선에서 멈췄습니다. 버그는 그보다 한 단계 더 나아간 경계에 존재했고, 동기식 테스트 실행기(synchronous test executor)가 바로 이 경계선을 보이지 않게 만든 원인이었습니다. 워커도 없고, 단축키도 없는 실제 스택만이 유일하게 정직한 증인입니다.
v0.2.0 필드 테스트는 **36/36 시나리오, 67/67 컨테이너 테스트, 50/50 부하 테스트**라는 결과를 가져왔습니다. 하지만 이것은 제가 실제 스택에 대해 엔드투엔드로 재실행하고, 제 녹색 단위 테스트 모음(green unit suite)을 더 이상 믿지 않게 되었기 때문이었습니다. 전체 보고서는 [field-test report](https://github.com/deghosal-2026/hiveplane/blob/main/docs/field-test/v0.2.0/FIELD_TEST_REPORT.md)와 [Docker test report](https://github.com/deghosal-2026/hiveplane/blob/main/docs/field-test/v0.2.0/DOCKER_TEST_REPORT.md)에서 확인할 수 있습니다.
## 참고 자료
- [HivePlane](https://github.com/deghosal-2026/hiveplane) — `pip install hiveplane==0.2.0`
- [Field test report (36/36)](https://github.com/deghosal-2026/hiveplane/blob/main/docs/field-test/v0.2.0/FIELD_TEST_REPORT.md) · [Docker report (67/67)](https://github.com/deghosal-2026/hiveplane/blob/main/docs/field-test/v0.2.0/DOCKER_TEST_REPORT.md)
- [Orchestration design](https://github.com/deghosal-2026/hiveplane/blob/main/docs/design/orchestration-design.md)
- [v0.2.0 릴리스 노트](https://github.com/deghosal-2026/hiveplane/blob/main/docs/release/v0.2.0/release-notes.md)
**당신의 테스트 스위트에서 가장 비용이 많이 드는 '녹색' 테스트, 즉 200과 행은 확인하지만 부작용(side effect)은 확인하지 않는 테스트가 무엇인지 찾아보세요. 가서 보세요. 기다릴게요.**
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기