LabHub
배우기 러닝패스 코스

Testing Tools in Practice

Inject failures to find resource leaks

LabHub 에서 이어서 보기

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

목표

정상·중복 종료·실패 경로와 앱 lifespan의 시작·정리를 관측합니다.

왜 중요한가

요청이 성공하면 연결을 닫는 코드는 있었지만 예외가 발생하면 열린 연결이 남았다. 성공 응답만 확인하는 테스트에서는 이 누수가 드러나지 않았다. 수명 주기 테스트는 반환값보다 자원이 언제 열리고 닫혔는지에 집중해야 한다.

단계

  1. /root/work/test-lifespan-cleanup-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: new_resource()는 {open:False, events:[]}인 새 딕셔너리이며 호출끼리 events를 공유하지 않습니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

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

mkdir -p /root/work/test-lifespan-cleanup-lab
test -e /root/work/test-lifespan-cleanup-lab/service.py || cp /opt/fixtures/ten_labs/test-lifespan-cleanup-lab/service.py /root/work/test-lifespan-cleanup-lab/service.py
test -e /root/work/test-lifespan-cleanup-lab/test_service.py || cp /opt/fixtures/ten_labs/test-lifespan-cleanup-lab/test_service.py /root/work/test-lifespan-cleanup-lab/test_service.py
cd /root/work/test-lifespan-cleanup-lab
  1. /root/work/test-lifespan-cleanup-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: start(resource)는 이미 열려 있으면 ValueError, 아니면 open=True로 바꾸고 events에 'open'을 추가합니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

  2. /root/work/test-lifespan-cleanup-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: stop(resource)는 열려 있을 때만 open=False로 바꾸고 'close'를 events에 추가합니다. 이미 닫혀 있으면 그대로 둡니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

  3. /root/work/test-lifespan-cleanup-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: read(resource)는 닫혀 있으면 RuntimeError, 열려 있으면 {ready:True}를 반환합니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

  4. /root/work/test-lifespan-cleanup-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: scope(resource)는 contextmanager입니다. 진입 시 start, 블록 안에는 resource를 yield하고 블록의 성공·실패 모두 stop으로 닫습니다. 블록 예외는 전파합니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

  5. /root/work/test-lifespan-cleanup-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: lifespan_for(resource)는 asynccontextmanager 함수 lifespan(app)을 반환합니다. scope(resource) 안에서 app.state.resource를 설정하고 yield합니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

  6. /root/work/test-lifespan-cleanup-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: create_app(resource)는 lifespan_for를 사용합니다. GET /ready는 app.state.resource를 read한 결과를 반환합니다. context 종료 시 자원을 닫아야 합니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

  7. /root/work/test-lifespan-cleanup-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: exercise(resource, fail=False)는 with TestClient(create_app(resource)) 안에서 GET /ready를 호출합니다. fail=True면 그 안에서 RuntimeError를 내고, 아니면 응답 JSON을 반환합니다. 두 경우 모두 자원이 닫혀야 합니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

참고

자원 상태를 독립적으로 만든다 — 테스트

/root/work/test-lifespan-cleanup-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: new_resource()는 {open:False, events:[]}인 새 딕셔너리이며 호출끼리 events를 공유하지 않습니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

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

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

변경 가능한 리스트를 전역이나 기본 인자로 공유하지 않습니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.

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

중복 시작을 거절한다 — 테스트

/root/work/test-lifespan-cleanup-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: start(resource)는 이미 열려 있으면 ValueError, 아니면 open=True로 바꾸고 events에 'open'을 추가합니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

두 번 시작해 자원 하나를 잃어버리는 동작을 거절합니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.

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

종료를 멱등하게 만든다 — 테스트

/root/work/test-lifespan-cleanup-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: stop(resource)는 열려 있을 때만 open=False로 바꾸고 'close'를 events에 추가합니다. 이미 닫혀 있으면 그대로 둡니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

여러 정리 경로가 겹쳐도 중복 close 이벤트가 나오지 않아야 합니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.

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

닫힌 자원의 사용을 막는다 — 테스트

/root/work/test-lifespan-cleanup-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: read(resource)는 닫혀 있으면 RuntimeError, 열려 있으면 {ready:True}를 반환합니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

준비 상태와 객체 존재 여부는 다릅니다. 객체가 있어도 닫혀 있을 수 있습니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.

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

예외 경로에 finally를 둔다 — 테스트

/root/work/test-lifespan-cleanup-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: scope(resource)는 contextmanager입니다. 진입 시 start, 블록 안에는 resource를 yield하고 블록의 성공·실패 모두 stop으로 닫습니다. 블록 예외는 전파합니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

yield 뒤에만 close를 쓰면 예외가 난 경우 그 줄에 도달하지 못합니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.

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

앱 수명주기와 자원을 연결한다 — 테스트

/root/work/test-lifespan-cleanup-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: lifespan_for(resource)는 asynccontextmanager 함수 lifespan(app)을 반환합니다. scope(resource) 안에서 app.state.resource를 설정하고 yield합니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

lifespan 함수 자체를 호출하는 것이 아니라 FastAPI 생성자에 전달합니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.

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

준비 상태를 실제 요청으로 읽는다 — 테스트

/root/work/test-lifespan-cleanup-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: create_app(resource)는 lifespan_for를 사용합니다. GET /ready는 app.state.resource를 read한 결과를 반환합니다. context 종료 시 자원을 닫아야 합니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

with TestClient를 사용해야 lifespan 시작과 종료를 모두 실행합니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.

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

요청 이후의 실패도 정리한다 — 테스트

/root/work/test-lifespan-cleanup-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: exercise(resource, fail=False)는 with TestClient(create_app(resource)) 안에서 GET /ready를 호출합니다. fail=True면 그 안에서 RuntimeError를 내고, 아니면 응답 JSON을 반환합니다. 두 경우 모두 자원이 닫혀야 합니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

정상 경로와 예외 경로를 같은 정리 구조로 묶으면 빠진 종료 경로를 줄일 수 있습니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.

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