틀린 구현을 잡는 반례
85분 안팎
학습 목표
길이 비교와 식별자 조회를 잘못 구현한 제공 변형을 JUnit 테스트로 검출합니다.
개념
통과하는 테스트에도 질문합니다
AI 변경이 정상 테스트를 통과했다고 해서 검사 능력이 충분하다고 볼 수는 없습니다. 이번에는 일부러 틀린 구현을 제공하고 같은 검사로 구별되는지 확인합니다. 길이 40을 거절하는 변형, 없는 번호를 성공으로 돌려주는 변형, 두 번째 완료를 미완료로 돌려주는 변형입니다. 정상 동작과 틀린 동작을 같은 입력으로 비교하면 빠진 반례를 찾을 수 있습니다. 변형은 학습용 fixture이며 실제 앱 소스를 자동 수정하는 mutation 프레임워크를 설치하지 않습니다.
검출 성공과 앱 실패는 다릅니다
정상 Candidate에서는 IndependentProbe가 끝까지 통과해야 합니다. 오류 Candidate에서는 명세를 비교한 단언이 실패해야 합니다. DetectionTest는 오류 Candidate에 대해 AssertionError가 발생했는지 검사합니다. 따라서 바깥 검사 전체가 성공한다는 말은 정상 앱이 통과하고 제공된 세 오류가 모두 검출되었다는 뜻입니다. 잘못된 앱을 그대로 배포한다는 뜻이 아닙니다. 보고에는 정상 통과와 변형 검출을 별도 열로 적어 의미가 뒤섞이지 않게 합니다.
실습 파일의 역할을 읽습니다
Candidate.java는 실제 TaskService를 감싸고 지정된 Mode에서만 오류를 주입합니다. IndependentProbe.java는 학습자가 명세에서 만든 검사를 넣는 자리입니다. DetectionTest.java는 NORMAL과 세 변형에 같은 probe를 적용하는 관문입니다. starter에는 정상 제목 생성과 첫 완료만 있어 정상은 통과하지만 변형 검출 세 검사가 실패합니다. 서비스의 기존 테스트가 많아도 새 probe에 필요한 반례가 없다는 상황을 재현합니다. 수정할 곳을 확인하고 fixture의 결함을 고쳐 검출을 숨기지 않습니다.
상한 등호를 구별하는 입력
BOUNDARY는 코드 포인트 40인 제목을 TITLE_TOO_LONG으로 거절합니다. 보통 제목인 복습이나 39글자는 이 결함을 드러내지 않습니다. 이모지 40개를 넣고 성공 Task를 literal 값으로 비교합니다. 정상 번호 2, 원래 제목, done=false까지 확인합니다. 예상하지 않은 예외를 assertDoesNotThrow로 감싸면 거절이 JUnit assertion 실패로 표현됩니다. Exception 전체를 잡아 조용히 반환하면 이 오류가 검출되었다는 증거를 잃으므로 그런 처리는 넣지 않습니다.
없는 번호를 정확한 실패로 비교합니다
MISSING_ID는 번호 99 요청을 성공 Task로 만듭니다. probe에서는 assertThrows로 IllegalArgumentException을 요구하고 메시지가 NOT_FOUND인지 확인합니다. 단순히 어떤 예외든 받으면 NullPointerException 같은 구현 결함이 의도한 실패로 오인될 수 있습니다. 오류 뒤 목록은 번호 1의 완료 상태 그대로여야 합니다. 반환 오류와 상태 보존은 다른 약속이므로 둘을 함께 비교합니다. 예외가 없을 때 assertion이 실패하는 과정도 보고서에서 읽습니다.
재완료의 반례는 순서가 만듭니다
REPEAT는 첫 완료에서는 정상 서비스 결과를 주고 두 번째에서는 done=false를 반환합니다. 첫 완료 검사만 있으면 변형이 정상과 구별되지 않습니다. 같은 Candidate에 두 번 완료를 요청하고 두 응답 모두 new Task(1, 복습, true)와 비교합니다. 반환값이 잘못되었다면 최종 저장 상태가 맞아도 계약 위반입니다. 반대로 응답만 맞고 저장 상태가 틀릴 수 있으므로 실제 서비스의 최종 목록도 확인합니다. 관찰 지점을 줄일 때 무엇을 놓치는지 생각합니다.
정상 구현부터 통과시킵니다
변형에서 실패한 검사만 보고 테스트가 좋다고 판단하면 안 됩니다. 정상 Candidate도 실패한다면 기대값이나 준비 순서가 잘못되었을 수 있습니다. 먼저 normalPasses를 통과시키고 같은 probe를 변형에 적용합니다. 정상 검사가 실패할 때에는 생성 번호와 앞선 상태가 예상대로 준비되었는지 확인합니다. 여러 기능을 공유하는 probe는 각 단계가 만든 번호를 정확히 알 필요가 있습니다. 오류 생성 뒤 다음 번호 3을 검사하여 거절이 번호를 소비하지 않았는지도 연결합니다.
검출 보고를 이름으로 읽습니다
boundaryDetected, missingDetected, repeatDetected는 서로 다른 요구를 다룹니다. Expected AssertionError to be thrown, but nothing was thrown 메시지는 변형을 실행했지만 probe가 아무 계약 차이도 지적하지 않았다는 뜻입니다. 오류 Candidate를 고쳐 정상으로 만드는 것이 해결이 아닙니다. 빠진 기대 결과를 probe에 추가합니다. 반대로 normalPasses의 expected와 actual 차이는 정상 동작 또는 검사 자체를 검토할 신호입니다. 바깥 테스트 이름과 안쪽 입력을 함께 읽어 대응을 정합니다.
동등한 변형이라는 가능성
어떤 변경은 요구 범위에서 결과를 바꾸지 않을 수 있습니다. 예를 들어 저장 구현의 내부 변수 이름만 바꾸면 어느 입력으로도 차이가 나지 않습니다. 이런 변형이 살아남았다고 반례가 부족하다고 단정하지 않습니다. 다만 이번 세 변형은 명세 위반 입력이 명확히 제공되어 있으므로 검출을 요구합니다. 일반 mutation 점수와 이번 세 fixture의 검출 개수를 같은 지표처럼 쓰지 않습니다. 숫자보다 변형이 어긴 약속과 구별 입력을 설명하는 것이 우선입니다.
테스트 우회를 검토합니다
assertThrows의 호출을 비우거나 DetectionTest를 Disabled로 표시하면 검사 실행 수가 줄어듭니다. 오류 fixture에서 무조건 AssertionError를 던지는 probe는 정상에서도 실패하므로 좋은 검사가 아닙니다. mode 이름을 보고 특정 변형에서만 실패하도록 분기하는 것도 입력·출력 계약을 검사하지 않습니다. probe는 Candidate의 add·complete·list만 호출하고 업무 결과를 비교합니다. scope.json의 허용 파일과 해시를 지켜 fixture와 기존 검사가 보존되었는지 확인합니다.
환경 오류와 assertion을 분리합니다
Permission denied는 mvnw 실행 권한을 확인할 문제입니다. Cannot access repository in offline mode는 준비된 캐시를 확인할 환경 문제이며 변형 검출 실패가 아닙니다. 컴파일 오류로 보고서가 없다면 테스트를 실행했다고 적지 않습니다. surefire 보고서의 tests·failures·errors·skipped를 읽고 실제 assertion 실패인지 확인합니다. 오류 Candidate가 낸 업무 예외는 의도된 assertion으로 감쌀 수 있지만 전체 빌드 오류를 검출 성공으로 기록하지 않습니다.
판단과 증거를 기록합니다
review.md에는 각 변형의 명세 위반, 사용 입력, 기대값, 검출한 테스트 이름을 씁니다. 실제 AI를 사용했다면 요청과 받은 변경을 남기고 계정 없이 수행했다면 제공 fixture 사용이라고 적습니다. 제출물은 probe 코드와 검출 표입니다. 정상 서비스 전체 회귀도 함께 통과시켜 새 검사 작성이 기존 동작을 훼손하지 않았는지 확인합니다. 이 실습이 확인한 세 오류 유형과 아직 검사하지 않은 동시성 등의 범위를 구별하여 다음 레슨의 회귀 계획에 넘깁니다.
따라하기
변형과 구별 입력을 정합니다
제공 변형의 위반을 읽고 witness 입력을 정합니다. 이 출력 자체는 검출 실행 결과가 아닙니다.
for mode, witness in [("BOUNDARY","40 이모지"),("MISSING_ID","없는 99"),("REPEAT","1 두 번 완료")]:
print(mode + " -> " + witness)실행 결과
BOUNDARY -> 40 이모지 MISSING_ID -> 없는 99 REPEAT -> 1 두 번 완료
자기 비교가 검출하지 못함을 확인합니다
재완료 기대는 true입니다. 오류값을 그대로 기대에 복사하지 않습니다.
normal = True
mutant = False
print("자기 비교:", mutant == mutant)
print("독립 기대:", mutant == normal)실행 결과
자기 비교: True 독립 기대: False
JUnit 검출 관문을 실행합니다
solution의 pom.xml 폴더에서 실행한 뒤 target/surefire-reports/TEST-lab.DetectionTest.xml을 확인합니다. normalPasses와 세 detected 검사가 모두 통과해야 합니다. starter에서는 변형 검출 검사 세 개의 assertion 실패를 확인하고 IndependentProbe에 반례를 추가합니다.
./mvnw test실제 검사 수와 검출 관문을 읽습니다
solution에서 Maven 검사 뒤 실행합니다. tests는 assertion 수가 아니라 검사 사례 수입니다. 정상 통과와 세 오류 변형 검출을 나눠 확인합니다. starter를 수정한 뒤에도 동일 명령을 실행합니다.
python3 report.py실행 결과
tests=42 failures=0 errors=0 skipped=0 boundaryDetected: PASS missingDetected: PASS normalPasses: PASS repeatDetected: PASS
확인 문제
실습
IndependentProbe.java에 40 이모지 허용, 없는 번호 99의 NOT_FOUND, 두 번 완료의 true 유지 검사를 추가합니다. Candidate와 DetectionTest는 유지합니다. review.md에 명세 근거와 각 변형의 구별 입력을 적습니다.
실행 명령
./mvnw test
기대 결과
tests=42 failures=0 errors=0 skipped=0 boundaryDetected: PASS missingDetected: PASS normalPasses: PASS repeatDetected: PASS; 종료 코드 0입니다.
모범 답안
모범 답안 내려받기더 읽기
면접 질문
- 구현과 같은 실수를 반복하지 않는 테스트를 만드는 방법을 설명해 주시면 됩니다.