LabHub
배우기 러닝패스 코스

Testing Tools in Practice

Contract tests for error-response regressions

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

알려진 오류·알 수 없는 오류·비공개 정보·추적 헤더를 테스트합니다.

왜 중요한가

오류 메시지 변경 뒤 모바일 앱의 재시도 로직이 망가졌다. 테스트는 예외가 발생했다는 사실만 확인했고 상태와 공개 본문의 의미는 비교하지 않았다. 내부 예외가 사용자에게 그대로 노출되는 회귀도 같은 틈을 통과했다.

단계

  1. /root/work/test-error-contract-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: DomainError(code, message)는 Exception 하위 클래스이며 .code에 code를 보관합니다. str(예외)는 message입니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

처음 한 번 준비하세요. 기존 파일은 덮어쓰지 않습니다.

mkdir -p /root/work/test-error-contract-lab
test -e /root/work/test-error-contract-lab/service.py || cp /opt/fixtures/ten_labs/test-error-contract-lab/service.py /root/work/test-error-contract-lab/service.py
test -e /root/work/test-error-contract-lab/test_service.py || cp /opt/fixtures/ten_labs/test-error-contract-lab/test_service.py /root/work/test-error-contract-lab/test_service.py
cd /root/work/test-error-contract-lab
  1. /root/work/test-error-contract-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: status_for(code)는 missing=404, conflict=409, invalid=422, 그 외=500입니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

  2. /root/work/test-error-contract-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: public_message(code)는 missing='Resource not found', conflict='State conflict', invalid='Invalid request', 그 외='Internal error'입니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

  3. /root/work/test-error-contract-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: request_id(value)는 ASCII 영문·숫자·밑줄·하이픈으로만 이루어진 1~32자 문자열이면 그대로, 아니면 'untracked'입니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

  4. /root/work/test-error-contract-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: problem(code, rid)는 type='urn:labhub:problem:'+code, title와 detail=public_message(code), status=status_for(code), request_id=request_id(rid)만 가진 딕셔너리입니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

  5. /root/work/test-error-contract-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: response_for(code, rid)는 problem을 본문으로, status_for를 상태로, application/problem+json을 media_type으로, X-Request-ID를 정규화한 rid로 둔 JSONResponse입니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

  6. /root/work/test-error-contract-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: install_handlers(app)는 DomainError 핸들러를 등록합니다. 헤더 X-Request-ID를 읽고 exc.code에 대해 response_for를 반환합니다. exc의 message는 응답에 넣지 않습니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

  7. /root/work/test-error-contract-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: create_app()은 핸들러를 설치하고 GET /fail/{code}에서 DomainError(code, 내부문장)를 냅니다. 단 code=boom이면 RuntimeError를 냅니다. RuntimeError 핸들러는 code=internal인 고정 500 문제 응답을 반환합니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

참고

업무 예외에 code를 남긴다 — 테스트

/root/work/test-error-contract-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: DomainError(code, message)는 Exception 하위 클래스이며 .code에 code를 보관합니다. str(예외)는 message입니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

처음 한 번 준비하세요. 기존 파일은 덮어쓰지 않습니다.

mkdir -p /root/work/test-error-contract-lab
test -e /root/work/test-error-contract-lab/service.py || cp /opt/fixtures/ten_labs/test-error-contract-lab/service.py /root/work/test-error-contract-lab/service.py
test -e /root/work/test-error-contract-lab/test_service.py || cp /opt/fixtures/ten_labs/test-error-contract-lab/test_service.py /root/work/test-error-contract-lab/test_service.py
cd /root/work/test-error-contract-lab

기계가 판단할 code와 사람이 보는 내부 메시지를 분리합니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.

저장 후 bash /opt/lab/checks/test-error-contract-lab/01-contract.sh로 확인하세요.

code를 상태로 매핑한다 — 테스트

/root/work/test-error-contract-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: status_for(code)는 missing=404, conflict=409, invalid=422, 그 외=500입니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

알 수 없는 code를 성공으로 취급하지 않습니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.

저장 후 bash /opt/lab/checks/test-error-contract-lab/02-contract.sh로 확인하세요.

공개 문장을 고정한다 — 테스트

/root/work/test-error-contract-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: public_message(code)는 missing='Resource not found', conflict='State conflict', invalid='Invalid request', 그 외='Internal error'입니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

예외 문자열에 DB 주소나 내부 경로가 들어 있어도 공개하지 않습니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.

저장 후 bash /opt/lab/checks/test-error-contract-lab/03-contract.sh로 확인하세요.

요청 id를 제한한다 — 테스트

/root/work/test-error-contract-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: request_id(value)는 ASCII 영문·숫자·밑줄·하이픈으로만 이루어진 1~32자 문자열이면 그대로, 아니면 'untracked'입니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

임의 헤더를 반사하지 않도록 길이와 문자 집합을 함께 제한합니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.

저장 후 bash /opt/lab/checks/test-error-contract-lab/04-contract.sh로 확인하세요.

문제 본문을 만든다 — 테스트

/root/work/test-error-contract-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: problem(code, rid)는 type='urn:labhub:problem:'+code, title와 detail=public_message(code), status=status_for(code), request_id=request_id(rid)만 가진 딕셔너리입니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

본문과 HTTP 상태가 서로 다르면 클라이언트가 어느 값을 믿을지 결정할 수 없습니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.

저장 후 bash /opt/lab/checks/test-error-contract-lab/05-contract.sh로 확인하세요.

응답 형식을 일관되게 만든다 — 테스트

/root/work/test-error-contract-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: response_for(code, rid)는 problem을 본문으로, status_for를 상태로, application/problem+json을 media_type으로, X-Request-ID를 정규화한 rid로 둔 JSONResponse입니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

단순 딕셔너리 반환은 오류도 200으로 만들 수 있습니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.

저장 후 bash /opt/lab/checks/test-error-contract-lab/06-contract.sh로 확인하세요.

업무 예외 핸들러를 연결한다 — 테스트

/root/work/test-error-contract-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: install_handlers(app)는 DomainError 핸들러를 등록합니다. 헤더 X-Request-ID를 읽고 exc.code에 대해 response_for를 반환합니다. exc의 message는 응답에 넣지 않습니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

예외를 잡는 위치를 흩어 놓지 말고 앱의 공통 경계에 둡니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.

저장 후 bash /opt/lab/checks/test-error-contract-lab/07-contract.sh로 확인하세요.

예상하지 못한 오류도 숨긴다 — 테스트

/root/work/test-error-contract-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: create_app()은 핸들러를 설치하고 GET /fail/{code}에서 DomainError(code, 내부문장)를 냅니다. 단 code=boom이면 RuntimeError를 냅니다. RuntimeError 핸들러는 code=internal인 고정 500 문제 응답을 반환합니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

테스트에서 예외 재전파를 끄고 실제 500 응답의 바이트를 확인합니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.

저장 후 bash /opt/lab/checks/test-error-contract-lab/08-contract.sh로 확인하세요.