LabHub
배우기 러닝패스 코스

Testing Tools in Practice

Reproduce time boundaries without sleeping

LabHub 에서 이어서 보기

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

목표

가짜 시계로 제한 창·재시도 시각·사용자별 격리를 검증합니다.

왜 중요한가

요청 제한 테스트에 sleep을 넣었더니 개발 PC에서는 통과하고 CI에서는 실패했다. 느린 실행기가 시간 경계를 바꾸고 테스트 자체도 오래 걸렸다. 시간은 프로그램의 입력이므로 호출자가 제어할 수 있게 만들고 경계를 정확히 밟아야 한다.

단계

  1. /root/work/test-clock-window-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: validate_limit(limit, window)는 bool 제외 양의 int limit와 양의 유한 int/float window만 허용해 (limit, float(window))를 반환합니다. 나머지는 ValueError입니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

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

mkdir -p /root/work/test-clock-window-lab
test -e /root/work/test-clock-window-lab/service.py || cp /opt/fixtures/ten_labs/test-clock-window-lab/service.py /root/work/test-clock-window-lab/service.py
test -e /root/work/test-clock-window-lab/test_service.py || cp /opt/fixtures/ten_labs/test-clock-window-lab/test_service.py /root/work/test-clock-window-lab/test_service.py
cd /root/work/test-clock-window-lab
  1. /root/work/test-clock-window-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: active(history, now, window)는 now-window보다 큰 시각만 원래 순서의 새 리스트로 반환합니다. history는 정렬된 비감소 시각입니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

  2. /root/work/test-clock-window-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: retry_after(history, now, window)는 이미 정리된 비어 있지 않은 history의 첫 시각+window-now를 ceil한 값과 0 중 큰 정수입니다. 빈 리스트는 0입니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

  3. /root/work/test-clock-window-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: history_for(state, key)는 없는 키면 빈 리스트, 있으면 그 기록의 사본을 반환합니다. 조회만으로 state를 수정하지 않습니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

  4. /root/work/test-clock-window-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: admit(state, key, now, limit, window)는 설정 검증 후 해당 키의 만료 기록을 정리합니다. 여유가 있으면 now를 추가하고 (True,0), 꽉 찼으면 추가하지 않고 (False,retry_after)를 반환합니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

  5. /root/work/test-clock-window-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: client_key(value)는 1~40자 ASCII 영문·숫자·하이픈 문자열을 그대로 반환하고 나머지는 ValueError입니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

  6. /root/work/test-clock-window-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: limited_response(wait)는 상태 429, 본문 {error:'rate_limited'}, Retry-After 헤더는 wait를 문자열로 만든 JSONResponse입니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

  7. /root/work/test-clock-window-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: create_app(clock, limit=2, window=10)는 GET /work에서 X-Client-ID를 검사해 잘못된 키는 400 {error:'invalid_client'}, 허용은 200 {ok:True}, 초과는 limited_response입니다. state는 앱별로 분리합니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

참고

설정을 검증한다 — 테스트

/root/work/test-clock-window-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: validate_limit(limit, window)는 bool 제외 양의 int limit와 양의 유한 int/float window만 허용해 (limit, float(window))를 반환합니다. 나머지는 ValueError입니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

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

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

bool은 int의 하위 타입입니다. NaN과 무한대도 따로 거절해야 합니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.

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

창의 왼쪽 경계를 제외한다 — 테스트

/root/work/test-clock-window-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: active(history, now, window)는 now-window보다 큰 시각만 원래 순서의 새 리스트로 반환합니다. history는 정렬된 비감소 시각입니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

정확히 만료된 시각을 남기는 >=와 >의 차이를 확인합니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.

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

대기 시간을 올림한다 — 테스트

/root/work/test-clock-window-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: retry_after(history, now, window)는 이미 정리된 비어 있지 않은 history의 첫 시각+window-now를 ceil한 값과 0 중 큰 정수입니다. 빈 리스트는 0입니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

0.2초 남았다고 Retry-After를 0으로 내면 클라이언트가 즉시 재요청합니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.

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

키마다 기록을 나눈다 — 테스트

/root/work/test-clock-window-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: history_for(state, key)는 없는 키면 빈 리스트, 있으면 그 기록의 사본을 반환합니다. 조회만으로 state를 수정하지 않습니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

공유 리스트를 반환하면 한 요청의 정리가 다른 요청의 기록을 바꿀 수 있습니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.

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

허용 요청만 기록한다 — 테스트

/root/work/test-clock-window-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: admit(state, key, now, limit, window)는 설정 검증 후 해당 키의 만료 기록을 정리합니다. 여유가 있으면 now를 추가하고 (True,0), 꽉 찼으면 추가하지 않고 (False,retry_after)를 반환합니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

거절된 요청을 추가하면 재시도할 때마다 만료 시각이 밀립니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.

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

클라이언트 키를 검증한다 — 테스트

/root/work/test-clock-window-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: client_key(value)는 1~40자 ASCII 영문·숫자·하이픈 문자열을 그대로 반환하고 나머지는 ValueError입니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

무제한 키 크기로 상태 메모리를 압박하지 않게 입력의 범위를 제한합니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.

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

거절 응답을 만든다 — 테스트

/root/work/test-clock-window-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: limited_response(wait)는 상태 429, 본문 {error:'rate_limited'}, Retry-After 헤더는 wait를 문자열로 만든 JSONResponse입니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

클라이언트가 재시도 시간을 알 수 있게 상태와 헤더를 함께 보냅니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.

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

가상 시간으로 요청 흐름을 끝낸다 — 테스트

/root/work/test-clock-window-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: create_app(clock, limit=2, window=10)는 GET /work에서 X-Client-ID를 검사해 잘못된 키는 400 {error:'invalid_client'}, 허용은 200 {ok:True}, 초과는 limited_response입니다. state는 앱별로 분리합니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

실제로 sleep하지 말고 리스트에 담은 현재 시각을 clock 함수로 전달합니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.

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