Devin.KR

테스트를 배포의 문으로 사용하기

55분 안팎

학습 목표

실패가 후속 단계를 차단하는 흐름을 만듭니다.

개념

배포 전의 질문을 실행 조건으로 바꿉니다

안내판 문장을 고친 뒤 이미지가 만들어졌다는 소식만 받으면, 기존 조회 기능이 유지되는지 알 수 없습니다. CI는 변경마다 정해진 검사를 반복하는 흐름입니다. 이 레슨의 목표는 테스트 실패를 발견하는 데서 끝내지 않고 이미지 빌드가 시작되지 않게 만드는 것입니다. 테스트와 빌드가 각각 성공했는지보다 어떤 조건에서 다음 명령으로 넘어갔는지를 확인합니다. 실행할 문을 하나씩 통제하면 신입도 실패한 변경의 전파 범위를 설명할 수 있습니다.

실습 환경과 경로를 한 번 준비합니다

이번 모듈은 Bash와 Python 3을 사용합니다. 로컬 레슨 zip은 압축을 풀고 루트에서 bash check.sh로 검사합니다. Java 서비스 전체는 미션 zip에 있으며 JDK 17과 포함된 Maven wrapper로 ./mvnw test를 실행합니다. 실제 이미지 빌드는 개인 Docker daemon에서만 수행하며 실행이 제한된 환경은 external 대기로 남깁니다. 브라우저 레슨은 표준 입력의 JSON을 읽고 한 줄 결과를 출력합니다. fixture는 가짜 입력이며 배포할 산출물로 사용하지 않습니다.

실패는 셸이 읽을 수 있는 상태여야 합니다

명령의 종료 상태 0은 그 명령이 성공했다고 보고한 것이며, 0이 아닌 값은 실패 신호입니다. 화면에 ERROR라는 글자가 있어도 프로세스가 0으로 끝나면 셸은 그 글자만 보고 멈추지 않습니다. 반대로 출력이 조용해도 비영 상태면 후속 작업을 차단해야 합니다. 테스트 프레임워크가 assertion 실패를 프로세스 실패로 전달하고, 실행 스크립트가 그 상태를 보존하는 두 경계가 연결되어야 합니다.

로그 복제가 실패 상태를 바꿀 수 있습니다

테스트 출력을 화면과 파일에 함께 남기려면 tee를 연결할 수 있습니다. 그러나 Bash 파이프라인은 기본적으로 마지막 명령의 상태를 사용합니다. 테스트가 7로 끝나고 tee가 0으로 끝나면 전체가 성공처럼 보일 수 있습니다. starter는 set -eu만 사용해 이 결함이 드러납니다. set -euo pipefail로 바꾸면 파이프 안의 실패가 전체 실패로 전달됩니다. 로그는 관측 자료이고 종료 상태는 진행 조건이라 각각 보존해야 합니다.

제공된 gate.sh의 인자를 읽습니다

gate.sh는 테스트 스크립트 경로, 빌드 스크립트 경로, 로그 경로를 순서대로 받습니다. 첫 명령은 bash로 테스트 파일을 실행하고 표준 오류까지 tee에 전달합니다. 다음 줄의 빌드 명령은 테스트 파이프가 통과한 경우에만 실행됩니다. 인자를 따옴표로 감싸므로 경로에 공백이 있어도 하나의 파일 경로로 전달됩니다. 명령 문자열을 eval로 실행하는 방식은 사용하지 않으며 과제의 입력은 직접 작성한 실행 파일로 제한합니다.

fixture로 차단을 관찰합니다

check-fixtures.py는 임시 디렉터리에 성공 테스트와 exit 7 실패 테스트를 만듭니다. 빌드 대역은 events 파일에 build라는 줄을 남깁니다. 성공에서는 종료 상태 0과 events 생성을 함께 확인하고, 실패에서는 비영 상태와 events 부재를 함께 확인합니다. 단순히 스크립트가 실패했다는 사실만 검사하면 이미 빌드가 실행된 뒤 마지막에 실패하는 잘못된 구현을 놓칠 수 있습니다. 부수 효과의 부재까지 관측합니다.

실패 로그도 제출 자료입니다

실패한 실행에서도 TEST-fail 표식이 test.log에 있어야 합니다. 로그가 없으면 왜 진행이 차단되었는지 동료가 재현하기 어렵습니다. 운영 비밀을 로그로 남기자는 뜻은 아닙니다. 이번 fixture는 공개 표식만 사용하며 실제 테스트 출력에도 토큰과 환경 파일 원문을 넣지 않습니다. 성공 로그만 보관하는 습관을 버리고 실패 원인을 설명하는 자료와 배포 가능한 산출물의 보관 정책을 나눕니다.

Maven 테스트 경계를 그대로 연결합니다

미션 ci.py는 ./mvnw -q package를 실행합니다. 이 서비스의 package 경로에는 기본 테스트가 포함되어 있으므로 실패하면 subprocess의 check 조건이 예외를 내고 Docker 호출에 도달하지 않습니다. 실제 학습자 명령에는 오프라인 옵션을 강요하지 않습니다. 작성자 검증은 캐시가 있는 환경에서 -o를 붙여 수행합니다. m03에서 제외한 external HTTP 테스트의 범위는 유지되며 이 성공을 실제 컨테이너 접속 성공으로 확대하지 않습니다.

테스트를 건너뛴 성공을 별도로 막습니다

종료 상태만 보면 테스트가 전혀 선택되지 않은 빌드도 통과할 수 있습니다. 다음 레슨에서는 실행 건수를 집계하고 실행 0건을 막습니다. 여기서는 실패 상태가 전달되는 첫 관문을 만들지만 그것이 검증 정책의 전부는 아닙니다. -DskipTests로 시간을 줄인 뒤 이미지가 생성되었다고 제출하면 검증한 변경만 남긴다는 목표를 충족하지 못합니다. 빠르게 만드는 것보다 확인한 범위를 정확하게 표시하는 것이 중요합니다.

자동 중단 옵션의 범위를 과장하지 않습니다

set -e는 모든 문맥의 실패를 동일하게 처리하는 규칙이 아닙니다. 조건문이나 일부 논리 연결식에서는 동작이 달라집니다. 제공 gate.sh는 테스트 파이프를 독립된 명령 줄에 놓아 실패하면 다음 줄이 실행되지 않는 구조로 좁혔습니다. if문으로 감싸고 실패 뒤에도 계속 진행하게 바꾸면 옵션 이름이 같아도 결과가 달라질 수 있습니다. 리팩터링 뒤에는 성공과 실패 fixture를 다시 실행하여 실제 경계를 확인합니다.

오류의 첫 위치를 읽습니다

AssertionError: gate fail이면 실패 fixture의 상태 또는 빌드 차단이 기대와 달랐다는 뜻입니다. 로그의 TEST-fail을 보고 tee 자체가 성공했는지와 pipefail 설정을 대조합니다. No such file or directory는 테스트 실패가 아니라 전달한 경로 또는 실행 위치 문제입니다. Permission denied는 파일 접근 조건을 살펴봅니다. 문제를 해결하려고 테스트 상태를 0으로 바꾸지 말고, 관측한 신호가 스크립트 끝까지 유지되도록 수정합니다.

시작 코드의 작은 결함만 수정합니다

starter의 gate.sh 첫 설정 줄을 고치고 bash check.sh를 다시 실행합니다. 검사 코드를 지우거나 실패 fixture를 성공 fixture로 바꾸면 완료로 인정하지 않습니다. 성공 경로도 계속 빌드를 실행해야 하므로 모든 경우에 exit 1로 끝내는 수정도 잘못입니다. 제출 설명에는 테스트 실패, 로그 저장, 빌드 차단의 관계를 자신의 말로 적고 어떤 실행 흔적이 그 주장을 뒷받침하는지 연결합니다.

배포는 아직 이 관문의 밖에 있습니다

이 모듈은 검증한 산출물을 만들고 전달하는 단계입니다. 실행 환경을 바꾸는 배포는 이후 모듈에서 readiness와 사용자 요청 검사를 거쳐 진행합니다. 따라서 실패 테스트 뒤 배포가 실행되지 않는다는 답은 빌드·산출물 생성 경계도 차단한다는 흐름으로 설명할 수 있습니다. 테스트가 통과해도 운영 상태가 건강하다고 단정할 수 없으며, 현재 관문은 소스 변경을 다음 단계에 넘길 최소 조건입니다.

더 읽기를 활용합니다

테스트 객체의 설계와 JUnit assertion 선택은 서재의 테스트 장에서 보완합니다. 이 레슨은 그 내용을 옮기지 않고 테스트 명령의 종료 상태를 CI가 읽는 방법에 집중했습니다. 테스트가 실제로 무엇을 확인했는지와 실패 시 어떤 단계가 생략되는지를 함께 설명하면 결과 화면의 초록 표시를 넘어 절차를 검토할 수 있습니다. 자신의 변경에서도 실패 fixture를 하나 남겨 다음 작성자가 차단 정책을 유지하는지 확인하게 합니다.

보고서 위치는 Maven Surefire 공식 안내에서 확인합니다.

따라하기

첫 설정 줄 읽기

압축 해제한 starter에서 pipefail이 빠진 위치를 읽습니다. 수정 전 bash check.sh가 gate fail에서 실패하는지 확인합니다.

cat gate.sh
bash check.sh

실패 상태 비교

이 코드는 임시 파일 없이 Bash 상태만 대조합니다. 첫 값은 기본 파이프의 상태이고 두 번째는 pipefail을 켠 상태입니다.

import subprocess
for setting in ['', 'set -o pipefail; ']:
    p = subprocess.run(['bash', '-c', setting + 'exit 7 | cat'])
    print(p.returncode)

실행 결과

0
7

설정 수정

gate.sh의 설정을 다음 줄로 바꾸고 테스트와 빌드 호출은 유지합니다.

set -euo pipefail

성공·실패 관문 검사

수정 뒤 실행합니다. 기록은 solution에서 직접 실행한 fixture 결과이며 Docker 성공 출력이 아닙니다.

bash check.sh

실행 결과

PASS gate pass
PASS log pass
PASS gate fail
PASS log fail
PASS report success
PASS report failure
PASS report error
PASS report zero
PASS report all-skipped
PASS report negative
PASS integrity original
PASS integrity tamper rejected
PASS CI artifact verified
PASS pipeline ok
PASS order ok
PASS fresh manifest ok
PASS pipeline failed
PASS order failed
PASS fresh manifest failed
PASS pipeline zero
PASS order zero
PASS fresh manifest zero
PASS pipeline skipped
PASS order skipped
PASS fresh manifest skipped
RESULT: 0 failure(s); fixture only, no Docker build

확인 문제

실습

gate.sh에 pipefail을 넣어 테스트 실패가 tee의 성공에 가려지지 않게 합니다. 실패 시 로그는 남고 빌드 이벤트는 없어야 합니다. 검사 코드는 수정하지 않습니다. 로컬 fixture는 실제 Docker 실행을 입증하지 않습니다.

시작 코드·테스트 내려받기

실행 명령

bash check.sh

기대 결과

모든 PASS와 RESULT: 0 failure(s); fixture only, no Docker build

모범 답안모범 답안 내려받기

더 읽기

면접 질문

  • CI에서 실패한 테스트 이후의 배포 동작을 설명합니다.