AI 개발 · 기본
AI 시대의 개발 공부 - 무엇을 배우고 어떻게 익힐까
테스트는 생각의 도구 - 반례를 먼저 찾는 습관
테스트를 '나중에 확인하는 일'이 아니라 '요구를 분명히 하는 일'로 보기, 경계값·빈 입력·이상한 입력 같은 반례 떠올리기, AI 가 만든 코드가 맞다는 증거를 요구하는 태도, unittest 기초, 예시 프로그램은 성적 등급 함수와 unittest 테스트를 함께 두고 경계값에서 실패하는 버그를 찾아 고친 결과를 출력한다
개발자KR · 원고 갱신
이 장에서 배우는 것
앞 장에서 입력과 출력, 예시와 완료 기준을 적으며 원하는 동작을 분명히 했다. 이제 그 기준을 코드에 질문할 차례다. “보통 잘 작동하는가”보다 “어디에서 약속을 어길 수 있는가”를 먼저 묻는다. 테스트(test)는 프로그램에 입력을 주고 실제 결과를 기대한 결과와 비교하는 일이다. 동시에 빠뜨린 요구를 발견하는 생각의 도구이기도 하다.
AI가 짧은 함수를 만들어 주면 겉모양은 그럴듯할 수 있다. 그러나 읽기 편한 코드와 요구를 지키는 코드는 같은 말이 아니다. 이 장에서는 학생의 학습 점검 도구에 들어갈 가상의 성적 등급 함수를 살펴본다. 경계에서 틀리는 함수를 테스트로 찾아낸 뒤, 고친 함수에 같은 질문을 다시 던진다. 예시의 점수와 등급 기준은 모두 설명용 가상값이다.
- 테스트를 실행 뒤의 확인뿐 아니라 요구를 구체화하는 과정으로 이해한다.
- 경계값, 빈 입력, 이상한 입력에서 반례를 찾는다.
- AI가 제시한 코드에 입력과 기대 결과가 드러나는 증거를 요구한다.
- 파이썬의 unittest로 테스트를 묶고 실패 결과를 읽는다.
- 수정 전후에 같은 테스트를 실행하고, 통과가 말해 주는 범위를 설명한다.
문제 상황
학생이 학습 기록장에 연습 문제 점수를 적는다. 기록장은 점수를 등급으로 바꾸어 보여 준다. 설명용 규칙은 90점 이상이면 A, 80점 이상이면 B, 70점 이상이면 C, 나머지는 D다. 점수는 0부터 100까지의 정수만 받기로 했다. 정수는 85처럼 소수 부분이 없는 수다.
AI가 만든 함수를 95점과 85점으로 실행해 본다. 각각 A와 B가 나오니 맞는 것처럼 보인다. 그런데 90점을 입력하자 B가 나온다. 함수에는 “90 이상”을 표현해야 할 자리에 “90보다 큼”이 들어 있었다. 90은 두 표현의 차이가 드러나는 입력이다. 이런 입력이 반례다. 반례는 “이 코드가 약속한 동작을 모두 지킨다”는 주장에 어긋나는 사례를 뜻한다.
실무에서도 비슷한 상황이 생긴다. 할인 조건, 신청 가능 나이, 제출 마감 시각처럼 어느 쪽에 포함되는지가 중요한 기준을 코드로 옮긴다. 평범한 사례 몇 개만 확인하면 기준 바로 위와 아래의 차이를 놓치기 쉽다. 코드 작성에 걸린 시간이 줄어들어도 포함 범위를 판단하고 결과를 확인하는 일은 남는다. 개발자의 일은 이러한 판단과 확인을 더 분명하게 드러내는 방향으로 바뀐다.
이 학생의 도구에는 빈칸을 제출했을 때의 동작도 필요하다. 아무 값이 없다는 뜻의 None을 받으면 D를 보여 주어야 할까, 입력을 다시 요구해야 할까. “등급을 계산한다”는 문장만으로는 결정할 수 없다. 테스트를 만들다 보면 코드의 오류뿐 아니라 아직 정하지 않은 요구도 만나게 된다.
테스트를 쓰면 요구의 빈틈이 보인다
테스트에는 최소한 입력과 기대 결과가 있어야 한다. 기대 결과는 실제 실행 결과와 비교할 정답이다. 먼저 요구에서 기대 결과를 정하고, 그다음 함수를 실행한다. 실행된 결과를 보고 그 값을 정답으로 적으면 현재 코드를 다시 확인하는 데 그치기 쉽다.
예를 들어 입력 90의 기대 결과를 A라고 쓰는 순간 “90점도 A에 포함한다”는 요구가 구체적인 질문이 된다. 입력 None의 기대 결과를 쓰려다가 망설인다면 빈 입력에 대한 정책이 빠져 있다는 뜻이다. 테스트를 작성하는 과정에서 요구를 보완하고, 보완한 요구를 코드에 반영한다.
| 확인할 요구 | 입력 | 기대 결과 |
|---|---|---|
| 보통의 B 구간 | 85 | B |
| A가 시작되는 점수 | 90 | A |
| B가 시작되는 점수 | 80 | B |
| C가 시작되는 점수 | 70 | C |
| 허용 범위의 양 끝 | 0, 100 | 각각 D, A |
| 빈 입력 | None | ValueError 발생 |
| 형식이나 범위가 다른 입력 | "90", -1, 101, True | ValueError 발생 |
ValueError는 값이 이 함수의 약속에 맞지 않는다는 사실을 알리는 예외다. 예외(exception)는 정상적인 계산을 계속할 수 없을 때 발생시키는 신호다. 여기서는 숫자처럼 보이는 문자열도 자동으로 바꾸지 않고 거절한다. 화면에서 입력받은 글자를 숫자로 바꾸는 일은 별도로 처리한다는 가정이다. 문자열은 따옴표로 감싼 "90"처럼 글자로 저장한 값이다.
빈 입력을 0으로 처리하는 도구도 있을 수 있다. 하지만 그러면 “아직 점수를 적지 않음”과 “실제로 0점을 받음”을 구분하기 어려워진다. 이 예제는 그 둘을 구분하려고 빈 입력을 거절한다. 정책은 도구의 목적에 따라 달라질 수 있지만, 정한 정책과 테스트와 코드가 같은 방향을 가리켜야 한다.
그림에서 기대 결과와 실제 결과는 서로 다른 곳에서 온다. 기대 결과는 요구에서, 실제 결과는 실행에서 얻는다. 이 둘을 비교해야 오류를 발견할 수 있다. AI에게 구현과 테스트를 함께 요청하더라도, 기대 결과가 요구에 맞는지는 사람이 다시 읽어야 한다.
반례는 경계와 빈자리에서 찾는다
경계값(boundary value)은 동작이 달라지는 기준에 놓인 값이다. 90점을 기준으로 A와 B가 나뉜다면 89, 90, 91이 좋은 질문이 된다. 기준 바로 아래, 기준 자체, 기준 바로 위를 나란히 놓으면 비교 연산의 의미가 드러난다. 비교 연산은 두 값의 관계를 검사하는 계산이다. >는 왼쪽 값이 더 큰지, >=는 더 크거나 같은지 묻는다.
빈 입력과 이상한 입력은 다른 질문을 던진다. 빈 입력은 “값이 없을 때도 계산하려 하는가”를 확인한다. 이상한 입력은 “받기로 한 종류와 범위를 벗어난 값에 어떻게 대응하는가”를 확인한다. 101은 정수지만 허용 범위 밖이고, "90"은 숫자처럼 보이지만 문자열이다. 둘은 거절되는 이유가 다르다.
True도 확인한다. True는 참을 나타내는 논리값이다. 파이썬에서는 일부 숫자 검사에서 정수와 비슷하게 취급될 수 있다. 이 함수는 점수로 정수만 받는다고 정했으므로 True도 거절한다. 코드의 검사 방식이 일상 언어로 적은 요구와 같은 뜻인지 확인할 필요가 있다.
반례를 찾는다고 해서 무작위로 많은 값을 넣어야 하는 것은 아니다. 먼저 입력을 의미 있는 묶음으로 나눈다. 정상 구간의 값, 등급이 바뀌는 값, 범위 끝의 값, 없는 값, 종류가 다른 값을 떠올린다. 각 묶음에서 어떤 잘못을 찾으려는지 한 문장으로 설명할 수 있으면 테스트를 읽기도 쉬워진다.
이 장의 완성 코드는 경계 자체인 90, 80, 70을 각각 검사한다. 짧은 예제에서 세 비교 조건의 오류를 분명하게 보여 주기 위해서다. 실제로 범위를 넓혀 확인할 때는 그림처럼 각 경계의 바로 아래와 위도 추가한다. 테스트 몇 개가 통과했다는 사실과 모든 가능한 입력을 확인했다는 사실은 구분해야 한다.
AI의 설명을 실행 가능한 증거로 바꾼다
AI에게 “코드가 맞는지 확인해 줘”라고만 요청하면 “정상적으로 작동한다”는 설명을 받을 수 있다. 설명은 검토의 출발점이지만 실행 기록을 대신하지 않는다. 입력과 기대 결과, 그 결과를 정한 근거를 요구하면 검토할 대상이 구체적으로 남는다. 다음 대화는 2026년 10월 기준 예시이며 특정 제품의 기능을 전제로 하지 않는다.
요청: 0부터 100까지의 정수 점수를 등급으로 바꾸는 함수를 검토하라. 90 이상은 A, 80 이상은 B, 70 이상은 C, 나머지는 D다. 빈 입력과 문자열과 범위 밖 값은 거절한다. 경계에서 틀릴 만한 입력을 고르고, 각 기대 결과를 요구의 어느 문장에서 얻었는지 설명하라. 실행하지 않았다면 실행했다고 쓰지 말라.
답의 예: 90은 A가 되어야 한다. “90 이상”에는 90도 포함되기 때문이다. 조건이 score > 90이면 90이 다음 조건으로 넘어갈 수 있다. 80과 70도 같은 방식으로 확인해야 한다. 이 답에서는 코드를 실행하지 않았으므로 실제 통과 여부는 확인하지 않았다.
이 답을 받은 학생은 90을 직접 실행하고 테스트에 남긴다. AI가 다른 경계값을 제안했더라도 기대 결과를 요구와 대조한다. “테스트가 통과했다”는 답을 받았다면 무엇을 실행했는지, 어떤 입력을 검사했는지 확인한다. 사용한 코드가 다르거나 입력이 빠져 있다면 그 결과를 자신의 프로그램에 그대로 적용할 수 없다.
unittest는 파이썬에 포함된 테스트 도구다. 이 예제에서는 함수 하나를 테스트 하나로 등록하는 FunctionTestCase를 사용한다. 새로운 클래스를 만들거나 상속 문법을 배우지 않아도 테스트를 묶어 실행할 수 있다. TestCase의 비교 기능은 미리 만들어진 객체를 통해 사용한다. 객체는 관련 기능을 함께 가진 값이며, 여기서는 두 값을 비교하거나 예외 발생을 확인하는 기능을 제공한다.
테스트 실패는 기대한 동작과 실제 동작이 어긋났다는 뜻이다. 실행 오류는 테스트를 진행하다 예상하지 못한 예외가 발생한 경우다. 이 예제에서는 실패 수와 오류 수를 따로 출력한다. 빈 입력에서 약속한 ValueError가 발생하는 것은 실패가 아니다. 그 신호를 내기로 정했으므로 해당 테스트는 통과한다.
완성 코드
다음 내용을 main.py에 저장한다. 파이썬 3.12 이상의 표준 라이브러리만 사용하며 추가 설치나 네트워크 연결은 필요 없다. 함수와 테스트를 한 파일에 함께 두었다. 수정 전후를 비교하기 위해 버그가 있는 함수도 남겨 두었으며, 실제 도구에 사용할 함수는 grade_fixed다. 프로그램은 두 함수에 같은 테스트를 적용하고 바로 끝난다.
import unittest
def validate(score):
if type(score) is not int:
raise ValueError("점수는 정수여야 한다")
if score < 0 or score > 100:
raise ValueError("점수는 0부터 100까지다")
def grade_buggy(score):
validate(score)
if score > 90:
return "A"
if score > 80:
return "B"
if score > 70:
return "C"
return "D"
def grade_fixed(score):
validate(score)
if score >= 90:
return "A"
if score >= 80:
return "B"
if score >= 70:
return "C"
return "D"
check = unittest.TestCase()
def test_normal():
check.assertEqual(grade_target(85), "B")
def test_boundary_90():
check.assertEqual(grade_target(90), "A")
def test_boundary_80():
check.assertEqual(grade_target(80), "B")
def test_boundary_70():
check.assertEqual(grade_target(70), "C")
def test_endpoints():
check.assertEqual(grade_target(0), "D")
check.assertEqual(grade_target(100), "A")
def test_empty():
check.assertRaises(ValueError, grade_target, None)
def test_unusual():
for score in ["90", -1, 101, True]:
check.assertRaises(ValueError, grade_target, score)
tests = [
test_normal, test_boundary_90, test_boundary_80,
test_boundary_70, test_endpoints, test_empty, test_unusual,
]
for label, grade_target in [("수정 전", grade_buggy), ("수정 후", grade_fixed)]:
suite = unittest.TestSuite()
for test in tests:
suite.addTest(unittest.FunctionTestCase(test))
result = unittest.TestResult()
suite.run(result)
print(f"{label}: 테스트 {result.testsRun}개, 실패 {len(result.failures)}개, 오류 {len(result.errors)}개")
for failed_test, detail in result.failures:
print(f" 실패: {failed_test.id()}")
print(f" 90점 등급: {grade_target(90)}")
줄별 해설
빈 줄은 함수와 작업 묶음을 구분하기 위한 여백이다. 위에서 아래로 읽으며 어떤 값이 어디로 이동하는지 따라가면 된다. 함수 정의 부분은 일을 수행할 방법을 저장한다. 실제 계산은 아래쪽 반복문에서 테스트를 실행할 때 시작된다.
첫 줄의 import unittest는 테스트 도구를 가져온다. def validate(score)는 점수가 약속에 맞는지 확인하는 함수를 정의한다. 괄호 안의 score는 함수를 호출할 때 전달받는 값의 이름이다. type(score) is not int는 그 값의 종류가 정수가 아닌지 확인한다. 이 검사에서는 문자열, None, True가 모두 거절된다.
raise ValueError로 시작하는 줄은 잘못된 입력을 알리고 현재 함수의 실행을 중단한다. 다음 if는 점수가 0보다 작거나 100보다 큰지 확인한다. or는 두 조건 중 하나라도 참인지 묻는다. 정수이면서 범위 안에 있으면 validate는 예외를 내지 않고 끝난다. 여기서 필요한 것은 계산 결과가 아니라 입력이 허용되는지에 대한 확인이다.
grade_buggy의 첫 줄은 validate(score)를 호출한다. 입력이 통과하면 등급 조건을 위에서 아래로 검사한다. return은 결과를 돌려주고 함수를 끝낸다. 95라면 첫 조건에서 A를 돌려주므로 아래 조건은 실행하지 않는다. 90은 첫 조건을 통과하지 못하고 두 번째 조건인 90 > 80에서 B를 받는다. 이 흐름이 첫 번째 버그다.
grade_fixed도 같은 순서로 검사하지만 세 조건에 >=를 사용한다. 90은 첫 조건에 포함되고, 80은 두 번째 조건에 포함되며, 70은 세 번째 조건에 포함된다. 앞의 조건부터 높은 등급 순서로 배치한 이유도 중요하다. 95는 80 이상이기도 하지만 먼저 A로 결정되어야 하기 때문이다.
check = unittest.TestCase()는 비교 기능을 사용할 값을 만든다. test_normal 함수의 assertEqual은 첫 번째 값과 두 번째 값이 같은지 확인한다. grade_target(85)의 결과가 "B"와 다르면 테스트 실패로 기록된다. grade_target은 아래 반복문에서 검사할 함수를 가리키는 이름이다. 수정 전에는 grade_buggy를, 수정 후에는 grade_fixed를 호출하게 된다.
test_boundary_90, test_boundary_80, test_boundary_70은 각 경계를 별도 테스트로 나눈다. 하나의 경계가 실패해도 나머지 경계 테스트를 실행할 수 있다. test_endpoints는 허용 범위 양 끝인 0과 100을 검사한다. 이 함수에는 비교가 두 번 들어 있지만 등록된 테스트 함수는 하나이므로 테스트 수에는 한 개로 잡힌다.
test_empty의 assertRaises는 지정한 예외가 발생하는지 확인한다. 첫 번째 인수는 기대하는 예외 종류, 두 번째는 실행할 함수, 세 번째는 그 함수에 전달할 값이다. 여기서는 grade_target에 None을 전달했을 때 ValueError가 발생해야 한다. grade_target(None)처럼 먼저 실행하지 않고 함수 이름과 입력을 나누어 전달한다.
test_unusual은 네 입력을 차례로 검사한다. 대괄호 안에 여러 값을 담은 것을 리스트라고 한다. for score in ...은 리스트의 값을 하나씩 score에 넣고 아래 줄을 실행한다. 네 입력 모두 ValueError를 내야 통과한다. 하나가 실패하면 그 테스트 함수의 나머지 검사는 진행되지 않는다. 입력마다 결과를 따로 보고 싶다면 나중에 테스트 함수도 각각 나눌 수 있다.
tests 리스트에는 테스트 함수의 이름을 담는다. 이름 뒤에 괄호가 없으므로 여기서는 함수를 실행하지 않는다. 아래 반복문은 이름표와 검사할 함수를 한 쌍씩 꺼낸다. 첫 번째 쌍은 "수정 전"과 grade_buggy이고 두 번째 쌍은 "수정 후"와 grade_fixed다. 각 차례마다 grade_target이 바뀐다.
TestSuite는 테스트를 담는 묶음이다. 안쪽 반복문은 각 함수를 FunctionTestCase로 감싸 묶음에 추가한다. TestResult는 실행 횟수, 실패, 오류를 모아 두는 결과 기록이다. suite.run(result)가 실제로 테스트를 실행한다. 두 번째 함수에 대해서는 새 묶음과 새 결과 기록을 만들어 앞의 실행과 섞이지 않게 한다.
마지막 출력에서 testsRun은 실행한 테스트 수다. len은 담긴 항목 수를 센다. failures에는 비교가 실패한 테스트가, errors에는 예상하지 못한 실행 오류가 담긴다. 실패 목록의 각 항목에는 테스트와 상세 설명이 들어 있다. 이 예제는 detail에 담긴 상세 설명 대신 failed_test.id()로 테스트 이름만 출력한다. 마지막 줄은 90점의 실제 등급을 보여 준다. f로 시작하는 문자열은 중괄호 안의 계산 결과를 글자 사이에 넣는다.
이 구조는 테스트가 검사할 함수를 실행 직전에 바꾸는 짧은 실험용 구성이다. 모든 테스트 함수가 정의된 다음, grade_target에 함수가 지정된 상태에서 실행되므로 이름을 찾을 수 있다. 이 장에서는 수정 전후에 같은 질문을 던진다는 점을 눈으로 확인하는 데 사용한다.
실행 결과
터미널에서 main.py가 있는 폴더로 이동한 뒤 다음 명령을 실행한다.
python3 main.py
예상 출력은 다음과 같다. 실행 시간이나 현재 시각을 출력하지 않으므로 같은 코드에서는 아래 결과가 반복된다.
수정 전: 테스트 7개, 실패 3개, 오류 0개
실패: test_boundary_90
실패: test_boundary_80
실패: test_boundary_70
90점 등급: B
수정 후: 테스트 7개, 실패 0개, 오류 0개
90점 등급: A
수정 전에도 일반 값, 양 끝 값, 빈 입력, 이상한 입력 테스트는 통과한다. 그 사실만 보고 함수 전체가 맞다고 판단하면 세 경계의 오류를 놓친다. 실패한 이름을 보면 90, 80, 70이 공통으로 등급 시작점이라는 사실이 드러난다. 코드를 읽으면 세 곳의 >가 요구의 “이상”과 어긋났음을 확인할 수 있다.
수정 후에는 준비한 일곱 테스트가 모두 통과한다. 이는 그 테스트가 확인한 입력에서 기대한 동작을 얻었다는 증거다. 모든 정수와 모든 종류의 값을 검사했다는 뜻은 아니다. 또한 이 프로그램은 실패를 보여 주는 실험이므로 실패 내용을 기록한 뒤 끝난다. 화면에 실패 수를 출력하는 것과 실행 명령 자체가 실패 상태로 끝나게 만드는 것은 별도의 설정이다.
실무에서 자주 틀리는 것
“이상”을 “초과”로 옮긴다
말로 적힌 포함 관계를 비교 기호로 바꿀 때 기준값을 빠뜨린다. 다음 두 조각은 각각 독립해서 실행할 수 있다. 같은 90을 넣었을 때 결과가 달라진다. 틀린 예는 B를, 고친 예는 A를 출력한다.
score = 90
grade = "B"
if score > 90:
grade = "A"
print(grade)
score = 90
grade = "B"
if score >= 90:
grade = "A"
print(grade)
고칠 때는 숫자를 임의로 낮추기보다 요구에 맞는 비교 기호를 선택한다. 90, 89, 91의 기대 결과를 먼저 써 보면 포함 관계를 다시 판단하기 쉽다.
현재 함수의 결과를 기대 결과로 사용한다
검사할 함수가 돌려준 값을 정답으로 사용하면 같은 오류를 양쪽에 복제한다. 다음 틀린 예는 요구상 정답이 A여도 통과한다. 예제의 함수는 문제를 드러내기 위해 항상 B를 돌려준다.
import unittest
def grade(score):
return "B"
check = unittest.TestCase()
expected = grade(90)
check.assertEqual(grade(90), expected)
import unittest
def grade(score):
return "B"
check = unittest.TestCase()
expected = "A"
check.assertEqual(grade(90), expected)
고친 예는 비교 단계에서 실패한다. 실패는 이 경우 원하는 발견이다. 기대 결과를 요구에서 정했기 때문에 잘못된 구현을 드러낼 수 있다. 테스트가 실패했다고 기대 결과를 B로 바꾸면 발견한 문제를 지우게 된다.
예외를 검사하기 전에 함수를 실행한다
assertRaises에 함수 실행 결과를 전달하려 하면 테스트 도구가 확인하기 전에 예외가 발생한다. 다음 틀린 예는 reject(None)을 먼저 실행하며 멈춘다. 고친 예는 함수와 입력을 나누어 전달하므로 기대한 예외를 확인하고 끝난다.
import unittest
def reject(score):
raise ValueError("입력을 거절한다")
check = unittest.TestCase()
check.assertRaises(ValueError, reject(None))
import unittest
def reject(score):
raise ValueError("입력을 거절한다")
check = unittest.TestCase()
check.assertRaises(ValueError, reject, None)
함수 이름만 쓰는 것과 이름 뒤에 괄호를 붙이는 것은 다르다. 전자는 실행할 대상을 전달하고, 후자는 그 자리에서 실행한다. 테스트 도구가 실행을 맡아야 예외 종류를 관찰할 수 있다.
한눈에 보기
| 관점 | 던질 질문 | 이 장의 예 | 판단할 범위 |
|---|---|---|---|
| 요구 구체화 | 무엇이 정답인가 | 90은 A다 | 기대 결과의 근거를 설명한다 |
| 경계 확인 | 기준값도 포함하는가 | 90, 80, 70 | 비교 조건의 포함 관계를 확인한다 |
| 입력 확인 | 없는 값과 다른 값은 어떻게 처리하는가 | None, "90", 101 | 정해 둔 거절 정책을 확인한다 |
| 실패 해석 | 어떤 약속과 어긋났는가 | 경계 테스트 세 개 실패 | 실패 입력과 해당 조건을 연결한다 |
| 수정 확인 | 같은 질문에 답이 바뀌었는가 | 수정 후 실패 0개 | 준비한 테스트 범위에서 수정 효과를 확인한다 |
| AI 결과 검토 | 실제로 무엇을 확인했는가 | 코드, 입력, 기대값, 실행 결과 | 설명과 실행 증거를 구분한다 |
테스트를 먼저 생각하면 코드를 읽는 목적도 분명해진다. 90이 왜 B가 되었는지 조건을 따라가고, 빈 입력이 왜 거절되는지 검사 순서를 살핀다. 다음 장에서는 이 작은 함수에서 시선을 넓혀 입력이 들어오고 처리되어 결과가 나가는 전체 흐름과 실패 지점을 살펴본다.
연습 문제
- 90점 경계를 확인하는 입력으로 89, 90, 91을 골랐다. 각각의 기대 등급을 쓰고, 셋 중 어떤 입력이 수정 전 함수의 오류를 드러내는지 설명하라.
- 80점 경계의 바로 아래와 위도 검사하려 한다. 완성 코드의 test_boundary_80에 두 비교를 추가하라. 비교 횟수가 늘었을 때 출력의 테스트 수가 어떻게 되는지도 설명하라.
- test_unusual에서 True를 빼도 다른 테스트가 모두 통과한다. 그렇다면 True를 받는 동작이 올바르다고 말할 수 있는지, 요구와 증거를 구분해 설명하라.
- AI가 “테스트가 모두 통과하니 오류가 없다”라고 답했다. 이 말을 더 정확한 문장으로 고치고, 이 예제에 추가할 테스트 입력 하나와 기대 결과를 제안하라.
정답과 해설
89는 B, 90은 A, 91은 A다. 수정 전 함수는 89와 91에는 기대한 등급을 주지만 90에는 B를 준다. 따라서 세 입력 중 90이 오류를 드러낸다. 바로 아래와 위가 맞아도 기준값의 포함 관계가 틀릴 수 있다.
다음 함수로 바꾼다. 79는 C, 81은 B가 되어야 한다. 테스트 함수 하나 안에 비교를 더했으므로 등록된 테스트 수는 여전히 7개다. 수정 전에는 80의 비교에서 실패하여 그 아래의 81 비교까지 진행하지 않는다. 각 입력의 결과를 독립해서 보고 싶다면 테스트 함수도 나눈다.
def test_boundary_80(): check.assertEqual(grade_target(79), "C") check.assertEqual(grade_target(80), "B") check.assertEqual(grade_target(81), "B")그렇게 말할 수 없다. 이 예제의 요구는 정수 점수만 허용하며 True는 거절하는 것이다. True 테스트를 빼면 그 동작에 대한 직접적인 실행 증거가 빠진다. 다른 테스트의 통과가 빠진 입력까지 보증하지 않는다. 요구를 바꾸려는 것인지, 검사만 생략한 것인지도 구분해야 한다.
“실행한 테스트의 입력에서는 기대한 결과와 일치했으며, 아직 확인하지 않은 입력이 남아 있다”로 고칠 수 있다. 추가 입력으로 90.0을 제안할 수 있다. 숫자 크기는 90과 같지만 소수 부분을 표현할 수 있는 실수이므로 이 함수의 정수 요구에 맞지 않는다. 기대 결과는 ValueError다. 이 테스트를 실제로 추가하고 실행한 뒤에야 해당 입력에 대한 증거를 얻는다.
READER FEEDBACK
질문·의견
내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.
댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.