LabHub
배우기 러닝패스 코스

테스트 도구 실전 · 설정 테스트의 전역 상태를 격리한다 · 이론

설정 테스트의 전역 상태를 격리한다의 설계 원리

LabHub 에서 이어서 보기

한 줄 요약

잘못된 환경값·기본값·스냅샷·비밀 필드 노출 회귀를 검증합니다.

개념 지도: 한 줄 요약 · 왜 이게 필요했나 · 어떻게 동작하나 · 계약을 읽고 실패를 예측하는 워크시트

왜 이게 필요했나

먼저 실행한 시험이 환경변수를 바꿔 뒤의 시험이 실패했다. 테스트 순서를 고정하면 문제가 숨겨졌지만 실제 앱의 설정 오타까지 정상 기본값으로 바뀌는 버그는 남았다. 입력 격리와 엄격한 파싱을 별도로 확인해야 한다.

어떻게 동작하나

각 시험에 새 환경 dict를 전달한다. 환경변수가 없을 때와 존재하지만 틀렸을 때를 분리하고 bool 문자열, 포트 범위, 유한 timeout을 확인한다. 앱 생성 후 원본 입력을 바꿔 스냅샷이 독립적인지 검사하고 공개 응답의 키 집합을 단언한다.

학생 테스트 → 정상 구현: 실제 시험 모두 통과           └→ 계약 위반 구현: 해당 동작에서 실패수집 실패·0개 실행·강제 종료 ≠ 결함 검출

계약을 읽고 실패를 예측하는 워크시트

다음은 구현을 통째로 외우는 답안이 아니라 단계별 코드 리뷰입니다. 각 변경 조각은 의도적으로 계약을 깨뜨립니다. 변경 후에도 정상 사례가 통과할 수 있다는 점에 주의하세요. 실행 전에 어느 입력·예외·상태를 관측하면 차이가 드러날지 예상하고, 구현 후에는 그 예상과 결과를 비교합니다.

1. 불리언을 명시적으로 파싱한다 — 테스트

제공된 service.py의 다음 공개 계약을 테스트하세요: parse_bool(value)는 대소문자를 무시한 true 또는 false만 bool로 반환합니다. 공백이 붙거나 문자열이 아니면 ValueError입니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

판단의 근거: bool('false')는 True입니다. 허용할 두 문자열을 직접 비교하세요. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.

리뷰할 잘못된 변경 조각:

bool(value)

이 조각이 들어간 함수의 공개 계약과 비교해 보세요. 성공 사례 하나로는 구분되지 않는다면 거절되어야 할 입력이나 실패 이후의 상태를 관측 대상으로 선택합니다.

2. 포트 범위를 검사한다 — 테스트

제공된 service.py의 다음 공개 계약을 테스트하세요: parse_port(value)는 ASCII 숫자만 있는 문자열을 int로 바꾸고 1~65535이면 반환합니다. 나머지는 ValueError입니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

판단의 근거: 정수 변환이 성공해도 유효한 포트 범위라는 뜻은 아닙니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.

리뷰할 잘못된 변경 조각:

<= 65536

이 조각이 들어간 함수의 공개 계약과 비교해 보세요. 성공 사례 하나로는 구분되지 않는다면 거절되어야 할 입력이나 실패 이후의 상태를 관측 대상으로 선택합니다.

3. 시간 제한을 유한 값으로 만든다 — 테스트

제공된 service.py의 다음 공개 계약을 테스트하세요: parse_timeout(value)는 문자열을 float로 변환해 0보다 크고 30 이하인 유한 값만 반환합니다. 나머지는 ValueError입니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

판단의 근거: NaN은 일반 비교에서 예상과 다르게 동작하므로 isfinite를 확인합니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.

리뷰할 잘못된 변경 조각:

0 <= number <= 30

이 조각이 들어간 함수의 공개 계약과 비교해 보세요. 성공 사례 하나로는 구분되지 않는다면 거절되어야 할 입력이나 실패 이후의 상태를 관측 대상으로 선택합니다.

4. 필수 비밀의 누락을 거절한다 — 테스트

제공된 service.py의 다음 공개 계약을 테스트하세요: required_token(env)는 TOKEN이 문자열이고 strip 후 비어 있지 않을 때 strip한 값을 반환합니다. 없거나 빈 값은 ValueError입니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

판단의 근거: 누락된 필수 비밀을 예제 기본값으로 대체하지 않습니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.

리뷰할 잘못된 변경 조각:

return value

이 조각이 들어간 함수의 공개 계약과 비교해 보세요. 성공 사례 하나로는 구분되지 않는다면 거절되어야 할 입력이나 실패 이후의 상태를 관측 대상으로 선택합니다.

5. 누락에만 기본값을 적용한다 — 테스트

제공된 service.py의 다음 공개 계약을 테스트하세요: load_settings(env)는 service=env의 SERVICE 또는 누락 시 'api', debug=parse_bool(DEBUG 누락 시 'false'), port=parse_port(PORT 누락 시 '8000'), timeout=parse_timeout(TIMEOUT 누락 시 '5'), token=required_token인 딕셔너리입니다. 빈 SERVICE는 ValueError입니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

판단의 근거: get의 기본값과 '값 or 기본값'은 빈 문자열 처리에서 다릅니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.

리뷰할 잘못된 변경 조각:

env.get("DEBUG","true")

이 조각이 들어간 함수의 공개 계약과 비교해 보세요. 성공 사례 하나로는 구분되지 않는다면 거절되어야 할 입력이나 실패 이후의 상태를 관측 대상으로 선택합니다.

6. 공개 설정에서 비밀을 제거한다 — 테스트

제공된 service.py의 다음 공개 계약을 테스트하세요: public_settings(settings)는 service와 debug만 가진 새 딕셔너리입니다. 원본은 수정하지 않습니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

판단의 근거: 토큰 값의 일부를 마스킹하는 것보다 필드 자체를 공개하지 않는 계약을 사용합니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.

리뷰할 잘못된 변경 조각:

"debug":settings["debug"],"token":settings["token"]}

이 조각이 들어간 함수의 공개 계약과 비교해 보세요. 성공 사례 하나로는 구분되지 않는다면 거절되어야 할 입력이나 실패 이후의 상태를 관측 대상으로 선택합니다.

7. 외부 변경과 설정을 분리한다 — 테스트

제공된 service.py의 다음 공개 계약을 테스트하세요: snapshot(env)는 load_settings 결과를 반환합니다. 호출 후 env를 수정해도 반환된 설정은 바뀌지 않습니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

판단의 근거: 앱 시작 시점의 설정을 나중에 바뀌는 입력 사전과 분리합니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.

리뷰할 잘못된 변경 조각:

return env

이 조각이 들어간 함수의 공개 계약과 비교해 보세요. 성공 사례 하나로는 구분되지 않는다면 거절되어야 할 입력이나 실패 이후의 상태를 관측 대상으로 선택합니다.

8. 시작 실패와 공개 응답을 확인한다 — 테스트

제공된 service.py의 다음 공개 계약을 테스트하세요: create_app(env)는 snapshot을 즉시 읽으며 잘못된 설정이면 ValueError로 앱 생성을 실패시킵니다. GET /info는 public_settings만 반환합니다. 다른 env로 만든 앱끼리 설정을 공유하지 않습니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

판단의 근거: 서버가 시작한 뒤 첫 요청에서 설정 오류가 드러나지 않도록 생성 시 검증합니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.

리뷰할 잘못된 변경 조각:

return settings

이 조각이 들어간 함수의 공개 계약과 비교해 보세요. 성공 사례 하나로는 구분되지 않는다면 거절되어야 할 입력이나 실패 이후의 상태를 관측 대상으로 선택합니다.

현장에서 만나는 모습

일반 딕셔너리로 환경을 주입하므로 실제 프로세스 전역 환경에 의존하지 않는다. 암호화된 비밀 저장소, 키 회전, 동적 재로딩까지 구현하는 예제는 아니다. 토큰 문자열은 학습용 입력이며 실제 운영 키를 실습 파드에 넣지 않는다. 제공 구현은 읽어도 되지만 채점은 별도 사본을 사용한다. 소스 문구 검사나 파일 수정으로 결함을 우회하지 말고 공개 인터페이스의 실행 결과를 검사한다.

다음 실습에서 할 것

여덟 단계가 하나의 실행 가능한 결과물로 이어집니다. 불리언을 명시적으로 파싱한다 — 테스트 → 포트 범위를 검사한다 — 테스트 → 시간 제한을 유한 값으로 만든다 — 테스트 → 필수 비밀의 누락을 거절한다 — 테스트 → 누락에만 기본값을 적용한다 — 테스트 → 공개 설정에서 비밀을 제거한다 — 테스트 → 외부 변경과 설정을 분리한다 — 테스트 → 시작 실패와 공개 응답을 확인한다 — 테스트.

각 단계는 함수나 파일이 존재한다는 사실이 아니라 실제 반환값·예외·상태 변화를 검사합니다. 정답을 본 뒤에는 일부러 경계 비교나 정리 코드를 바꾸어 어떤 시험이 실패하는지 확인하세요. 앞선 시험이 다음 단계에서도 유지되는 이유를 설명하고, 이 실습이 보장하지 않는 운영 조건을 한 가지 적어 보세요.