실패 시험에 오류 이름을 붙였다고 그 오류를 검증한 것은 아니다. 준비 중 먼저 끝나거나 잘못된 자료 때문에 막히면 정작 보고 싶은 경쟁은 일어나지 않을 수 있다. 예약 잠금의 구현은 앞에서 다뤘다. 이번에는 실패가 실제로 어디에서 발생했는지 확인하는 시험 설계에 초점을 맞췄다.

아무 저장이나 실패시키면 질문이 달라진다

예약의 running 저장 실패를 보고 싶다면 save 대역에서 status가running인 경우에만 오류를 내야 한다. 생성이나 초기 목록 저장부터 실패하면 dispatch 직전 잠금 정리를 시험한 것이 아니다. 기존 시험은 실패할 상태와 횟수를 지정하고 실행 호출이0인지 확인한다.

최종 저장 실패 시험은 먼저 실행 번호 checkpoint까지 남긴 뒤 그 이후 저장을 실패시킨다. 같은 save 오류여도 전달 전과 전달 뒤는 재전송 판단이 다르다. 나는 오류 코드뿐 아니라 주입 조건과 이미 통과한 단계도 근거로 읽었다. 실패 위치를 고정해야 다음 tick의 호출 수가 왜 늘면 안 되는지 설명할 수 있다.

시간을 추측하기보다 대기 문을 둔다

취소와 실행이 겹치는 시험은 entered와hold 같은 promise 문을 사용한다. 취소 저장에 먼저 들어간 경우에는 그 저장을 붙잡은 채 tick을 시작하고, 실행이 먼저 running 저장에 들어간 경우에는 반대 순서로 취소를 요청한다. 우연히 빠른 쪽이 이기는 한 번의 sleep 결과로 두 경우를 대신하지 않는다.

취소 선행에서는 실행0과 취소 상태 보존, 실행 선행에서는 취소 거절과 성공 결과 보존을 각각 본다. 다른 프로젝트가 진행하는지도 별도로 확인한다. 이 시험이 모든 스레드와 전원 차단 순서를 탐색하는 것은 아니다. 검증한 범위는 명시한 두 순서에서 의도적으로 만든 상태 전이다.

준비 중 종료도 반환 뒤의 행동을 확인한다

NotebookService 시험은 Python 환경 준비나 초기 노트북 load를 일부러 기다리게 만든다. 그 사이 shutdown을 보낸 뒤 대기를 풀고 run과 종료가 끝나는 것을 함께 기다린다. 그 다음 busy가 False이고 kernel이 없는지, 중지 안내가 남았는지를 본다.

shutdown 뒤에 준비 작업이 늦게 끝나도 새 프로세스를 만들지 않았는지 확인한다. 이 방식은 실제 모델이나 외부 서비스를 호출하지 않고 생명주기의 틈을 검사한다. 준비 함수에 전달한 실행 경로도 시험용이며 사용자 Python 환경을 끊는 실험은 아니다. 어떤 자원을 만들지 않아야 했는지까지 결과에 적는다.

복사 실패 뒤에는 이웃과 재시작도 본다

별도 Electron 복사 시험은 소유한 DB에 존재하지 않는 작업 폴더를 지정하고 ENOENT가 실제 발생했는지 assert한다. 실패 뒤 프로젝트와 탭 목록, 원본 초안과 동료 프로젝트를 이전 상태와 대조한다. 미완성 프로젝트가 없는지만 보는 것으로 끝내지 않는다.

동료의 실제 Python 출력이 계속 나오는지, 정상 재시작 뒤에도 미완성 복사본이 없는지 확인한다. 이후 누락 폴더를 복구하고 같은 원본의 새 복사를 시험한다. 기존 실패를 성공으로 바꾸는 것이 아니라 조건을 바꾼 후속 시도의 근거다. 파일 시스템 오류를 코드 대역 하나로 흉내 낸 경우와 실제 fixture 경로에서 일으킨 경우도 나눠 읽는다.

합성 실험에도 도달 여부를 공개한다

공유 실패 실험실은 fault_requested와fault_exercised를 나눠 둔다. 마지막 기록 오류를 요청했어도 그 전에 취소되면 도달하지 않았다고 남긴다. 전달 횟수, 읽기 확인과 최종 기록을 함께 볼 수 있지만 디스크 내구성이나 실제 브라우저 시험은 아니다.

이번 집필에서는 원래 시험을 읽고 독립 합성 모듈만 실행했다. 운영 앱에 장애를 주입하지 않았다. 실패 후 정리, 이웃의 진행과 재전달 금지를 별도 질문으로 두면 시험이 무엇을 보장하는지 명료해진다. 정상 경로의 성공만 늘리기보다 의도한 실패가 정말 일어났고 그 이후 어떤 상태가 남았는지를 확인하는 것이 내 검수의 기준이 됐다.