검사 붙이기 - 고칠 때마다 돌리는 확인 목록
이 장에서 배우는 것
앞 장에서 가계부의 월별 보고서를 HTML 파일로 만들었다. 화면이 보기 좋게 나와도 합계가 맞는지는 따로 확인해야 한다. 같은 금액을 여러 번 더하거나, 다음 달의 기록을 이번 달에 섞어도 화면 자체는 만들어질 수 있기 때문이다. 이번에는 사람이 매번 눈으로 살피던 확인 목록을 Python 코드로 옮긴다.
검사에는 Python에 기본으로 들어 있는 unittest를 사용한다. 검사할 입력과 기대하는 답을 정해 두고, 실제 답이 같은지 비교하는 도구다. 외부 패키지를 설치하지 않는다. 완성 파일을 한 번 실행하면 정상 검사, 일부러 넣은 버그를 잡는 검사, 원래 규칙으로 돌아온 검사가 차례로 끝난다.
- 핵심 함수에 입력을 주고 기대한 결과와 비교한다.
- 금액, 날짜, 월 경계, 예산 경계의 사례를 표로 정리한다.
- 일부러 잘못된 예산 규칙을 넣어 검사가 실패하는지 확인한다.
- main 함수에서 검사를 실행하고 일정한 형식으로 결과를 출력한다.
이번 장에서 만들 것
혼자 쓰는 가계부에서 기록을 만들고, 한 달의 합계를 계산하고, 예산 초과 여부를 판단하는 부분을 검사한다. 앞 장의 파일이 없어도 실행되도록 필요한 기능을 main.py에 함께 넣는다. 저장, 검색, 화면 꾸미기는 이번 확인 목록에 추가하지 않는다. 화면에 넣을 숫자를 결정하는 작은 함수에 집중한다.
예산 규칙은 먼저 한 문장으로 고정한다. “합계가 한도보다 클 때만 예산 초과다.” 합계와 한도가 같으면 초과가 아니다. 이 문장이 없으면 AI가 작성한 코드와 검사 코드가 서로 다른 뜻으로 만들어질 수 있다. 이번 시연에서는 바로 이 차이를 이용해 버그를 넣는다.
사용자 입력이나 현재 시각은 사용하지 않는다. 기록의 날짜와 금액은 코드에 고정한다. 파일을 읽지 않으므로 임시 폴더도 필요 없다. 실행 중에 의도한 검사 실패가 한 번 나오지만, 마지막에는 정상 규칙의 검사가 모두 통과해야 한다.
| 대상 | 하는 일 | 확인 기준 |
|---|---|---|
| make_record | 날짜·금액·분류로 기록 생성 | 잘못된 입력을 거절한다 |
| monthly_total | 지정한 달의 금액 합산 | 다른 달의 기록을 제외한다 |
| is_over_budget | 합계와 예산 한도 비교 | 같으면 거짓, 넘으면 참이다 |
| run_checks | 검사 묶음을 실행하고 요약 | 검사 수와 실패 수를 출력한다 |
따라하기
아래 요청문을 순서대로 보낸다. AI의 답은 매번 다를 수 있다. 답변에 같은 설명이 나왔는지보다 실제 파일을 실행했을 때 약속한 결과가 나오는지를 확인한다. 이 장의 실행 결과를 그대로 재현하려면 단계 1에서 뒤의 완성 코드를 main.py에 복사한다. 이후 단계에서는 해당 파일을 읽고 확인하며, 필요 없는 수정은 하지 않는다.
단계 1. 실행할 파일과 규칙을 고정한다
① 보낼 요청문
Python 3.12 이상에서 표준 라이브러리만 쓰는 가계부 검사 예제를 만들겠다. 파일은 main.py 하나다. 날짜는 YYYY-MM-DD, 금액은 양의 정수, 분류는 식비·교통·생활이다. 합계가 예산 한도보다 클 때만 초과로 판단한다. unittest 검사 8개와 의도한 버그 시연을 포함한다. 아래에 붙이는 완성 코드를 기준으로 각 함수의 역할을 설명하고, 아직 파일을 바꾸지는 마라.
② AI가 돌려줄 내용의 요지
답은 매번 다를 수 있다. 기록 생성, 월별 합계, 예산 비교와 검사 실행을 나누어 설명한다. 예산과 같은 금액은 초과가 아니라는 규칙을 확인한다.
③ 직접 확인할 것
뒤의 완성 코드를 main.py에 저장한다. 터미널에서 다음 명령을 실행한다. 터미널 명령은 Python 코드 안에 넣지 않는다.
python3 main.py
출력 첫 줄이 “정상 규칙: 검사 8개, 실패 0개, 오류 0개”인지 확인한다. 전체 출력은 뒤의 실행 결과와 비교한다. 파일을 처음부터 독립적으로 실행할 수 있다는 사실을 먼저 확보하는 단계다.
④ 체크포인트
추가 설치나 입력 대기 없이 실행이 끝나면 다음 단계로 간다. 문법 오류가 나면 새 기능을 부탁하기 전에 복사한 들여쓰기와 따옴표부터 확인한다.
단계 2. 경계 사례를 표로 확인한다
① 보낼 요청문
main.py를 읽고 검사할 입력과 기대 결과를 표로 정리해라. 특히 금액 0과 음수, 존재하지 않는 날짜, 허용하지 않은 분류, 월말과 다음 달 첫날, 기록이 없는 달, 예산과 같은 금액, 예산보다 1원 많은 금액을 포함해라. 아래 경계 사례 표와 코드가 일치하는지 확인하고 파일은 바꾸지 마라.
② AI가 돌려줄 내용의 요지
답은 매번 다를 수 있다. 정상 입력뿐 아니라 규칙이 바뀌는 지점의 입력을 나열한다. 예산 5,000원에 합계 5,000원과 5,001원을 따로 확인해야 한다고 설명한다.
| 상황 | 입력 | 기대 결과 |
|---|---|---|
| 정상 기록 | 2026-10-01, 3,000원, 식비 | 입력한 값으로 기록 생성 |
| 허용하지 않는 금액 | 0원, -1원, True, 1.5 | ValueError 발생 |
| 없는 분류 | 여행 | ValueError 발생 |
| 없는 날짜 | 2026-02-30 | ValueError 발생 |
| 월 경계 | 10월 31일과 11월 1일 | 10월 합계에는 10월 기록만 포함 |
| 빈 달 | 11월 기록만 있는데 10월 조회 | 합계 0원 |
| 한도와 같음 | 합계 5,000원, 한도 5,000원 | 초과 아님 |
| 한도보다 큼 | 합계 5,001원, 한도 5,000원 | 초과 맞음 |
③ 직접 확인할 것
python3 main.py
정상 규칙의 검사 수가 8개인지 다시 확인한다. 표의 모든 입력이 각각 별도 검사 하나를 뜻하지는 않는다. 금액 검사 하나는 네 입력을 작은 묶음으로 나누어 확인한다.
④ 체크포인트
표의 기대 결과를 코드 없이도 말할 수 있으면 다음 단계로 간다. 기대 결과를 모르겠다면 검사를 추가하기 전에 가계부 규칙부터 정한다.
단계 3. 정상 값과 잘못된 입력을 검사한다
① 보낼 요청문
make_record의 검사 네 개를 설명해라. assertEqual은 실제 값과 기대 값을 비교하고, assertRaises는 지정한 예외가 발생하는지 확인한다는 뜻부터 풀어 써라. 금액 검사에서 True까지 거절하는 이유도 설명해라. 파일은 바꾸지 마라.
② AI가 돌려줄 내용의 요지
답은 매번 다를 수 있다. 올바른 기록은 내용 전체를 비교한다. 잘못된 금액·분류·날짜는 ValueError라는 입력 오류 신호가 나오는지 확인한다. Python에서 True가 정수처럼 취급되는 경우가 있어 금액의 자료형을 정확히 확인한다고 설명한다.
③ 직접 확인할 것
python3 main.py
정상 규칙에서 오류가 0개인지 확인한다. 여기서 예외란 함수가 정상 결과를 돌려주는 대신 문제를 알리는 신호다. assertRaises 안에서 예상한 ValueError가 발생하면 검사는 통과한다. 반대로 잘못된 입력을 조용히 받아들이면 검사는 실패한다.
④ 체크포인트
잘못된 입력을 넣는 검사도 통과할 수 있다는 뜻을 이해하면 다음 단계로 간다. 프로그램의 입력 거절과 검사 자체의 실패를 구분하는 것이 핵심이다.
단계 4. 월 합계와 예산 경계를 검사한다
① 보낼 요청문
monthly_total과 is_over_budget의 검사 네 개를 읽어라. 월 합계 검사에는 10월 1일, 10월 31일, 11월 1일을 넣고 기대 합계를 숫자로 직접 적은 이유를 설명해라. 예산 비교는 같은 금액과 1원 초과를 각각 검사한다. 기대 값을 검사 대상 함수로 다시 계산하지 마라.
② AI가 돌려줄 내용의 요지
답은 매번 다를 수 있다. 같은 달의 두 기록은 더하고 다음 달 기록은 제외한다. 기대 합계 5,000원을 직접 적어야 함수가 잘못 계산했을 때 차이가 드러난다. 예산 경계 양쪽을 확인하면 비교 기호가 바뀌는 실수를 찾을 수 있다.
③ 직접 확인할 것
python3 main.py
정상 검사와 복구 검사 모두 실패가 0개인지 확인한다. 코드에서도 월 합계의 기대 값이 5,000인지 읽는다. 검사 통과만 보고 끝내지 않고 실제로 무엇을 비교했는지 살피는 과정이다.
④ 체크포인트
합계에 11월의 9,000원이 들어가면 어느 검사가 실패할지 짚을 수 있으면 다음 단계로 간다.
단계 5. 일부러 넣은 버그가 잡히는지 본다
① 보낼 요청문
main.py의 buggy_budget 함수가 정상 함수와 어떻게 다른지 설명해라. 같은 검사 목록에 이 함수를 전달했을 때 test_budget_equal만 실패하는지 확인해라. 이 시연을 통과시키려고 기대 값을 바꾸거나 검사를 삭제하지 마라. 정상 함수는 그대로 유지해라.
② AI가 돌려줄 내용의 요지
답은 매번 다를 수 있다. 정상 함수는 >를 쓰고 버그 함수는 >=를 쓴다. 한도와 같은 금액에서만 판단이 달라져 test_budget_equal이 실패한다. 검사가 실제 실수를 감지한다는 증거가 된다고 설명한다.
③ 직접 확인할 것
python3 main.py
“버그 주입: 검사 8개, 실패 1개, 오류 0개” 다음에 “실패 항목: test_budget_equal”이 나와야 한다. 이 장에서 버그 주입이란 정상 함수 대신 일부러 틀린 함수를 검사에 넘기는 일이다. 원래 함수의 내용을 덮어쓰지 않으므로 복구를 잊을 위험도 줄어든다.
④ 체크포인트
실패 항목이 예상과 같으면 다음 단계로 간다. 실패가 0개라면 검사에 실제로 어떤 함수를 전달했는지 확인한다. 실패가 여러 개라면 버그 함수에 다른 차이가 들어갔는지 비교한다.
단계 6. main에서 검사 결과를 일정하게 출력한다
① 보낼 요청문
run_checks와 main의 연결을 설명해라. unittest의 기본 보고서는 StringIO에 받아 두고 검사 수·실패 수·오류 수만 출력하는 이유를 설명해라. 매번 새 검사 묶음을 만들고, 예상과 다른 결과이면 종료 상태를 1로 만드는 흐름도 확인해라. 파일은 바꾸지 마라.
② AI가 돌려줄 내용의 요지
답은 매번 다를 수 있다. 기본 보고서에는 실행 시간이 들어갈 수 있으므로 그대로 화면에 내보내지 않는다. 실행 결과 객체에서 필요한 수만 읽어 출력한다. 의도한 실패와 예상 밖의 실패를 main에서 구분한다고 설명한다.
③ 직접 확인할 것
python3 main.py
출력에 실행 시간이 없는지 확인한다. 이어서 아래 명령으로 문법 검사를 할 수 있다. -W error는 경고도 오류로 취급하라는 옵션이다. 성공하면 별도 출력 없이 끝나며, 컴파일 결과를 보관하는 폴더가 생길 수 있다.
python3 -W error -m py_compile main.py
④ 체크포인트
실행 결과가 뒤의 예시와 같고 문법 검사도 끝나면 다음 단계로 간다. 실패 수는 기대 값과의 불일치이고, 오류 수는 예상하지 못한 예외 등으로 검사 진행에 문제가 생긴 경우다.
단계 7. 변경 내용을 검토하고 다시 실행한다
① 보낼 요청문
처음 저장한 main.py와 현재 파일의 변경 전후 차이인 diff를 검토해라. 버전 관리 도구를 쓰지 않는다면 두 파일 내용을 직접 비교해라. 기대 값을 실제 결과에 맞춰 바꾼 부분, 삭제된 검사, 예산 비교 기호의 변경이 있는지 확인해라. 마지막으로 정상·버그 주입·복구 결과가 각각 0개·1개·0개의 실패인지 점검해라.
② AI가 돌려줄 내용의 요지
답은 매번 다를 수 있다. 변경이 없으면 없다고 답한다. 변경이 있으면 함수의 수정과 검사의 수정을 구분해 설명한다. 통과시키기 위해 기대 값만 바꾸지 않았는지 확인한다.
③ 직접 확인할 것
python3 main.py
마지막 줄이 “확인 목록 시연 완료”인지 확인한다. AI가 실행했다고 답해도 자신의 터미널에서 직접 실행한다. 검사가 통과하더라도 변경된 줄을 읽어 요청하지 않은 수정이 없는지 확인한다.
④ 체크포인트
정상 검사 통과, 의도한 실패 감지, 복구 검사 통과를 모두 확인하면 끝이다. 이후 가계부 코드를 고칠 때 같은 확인 목록을 다시 돌리는 출발점이 마련된다.
완성 코드
아래 전체를 main.py로 저장한다. 검사 대상 함수에 전달하는 자료는 이 파일에서 만든 기록으로 한정한다. 월 조회 값도 정해진 YYYY-MM 형식을 사용한다. 여기서 그 밖의 입력 규칙까지 새로 늘리지는 않는다.
import io
import unittest
from datetime import date
CATEGORIES = ("식비", "교통", "생활")
def make_record(day, amount, category):
parsed = date.fromisoformat(day)
if parsed.isoformat() != day:
raise ValueError("날짜는 YYYY-MM-DD 형식이어야 한다")
if type(amount) is not int or amount <= 0:
raise ValueError("금액은 양의 정수여야 한다")
if category not in CATEGORIES:
raise ValueError("허용하지 않은 분류다")
return {"date": day, "amount": amount, "category": category}
def monthly_total(records, month):
return sum(
record["amount"]
for record in records
if record["date"][:7] == month
)
def is_over_budget(total, limit):
return total > limit
def make_suite(budget_check):
class LedgerChecks(unittest.TestCase):
def test_record_valid(self):
record = make_record("2026-10-01", 3000, "식비")
self.assertEqual(
record,
{
"date": "2026-10-01",
"amount": 3000,
"category": "식비",
},
)
def test_record_amount(self):
for amount in (0, -1, True, 1.5):
with self.subTest(amount=amount):
with self.assertRaises(ValueError):
make_record("2026-10-01", amount, "식비")
def test_record_category(self):
with self.assertRaises(ValueError):
make_record("2026-10-01", 3000, "여행")
def test_record_date(self):
with self.assertRaises(ValueError):
make_record("2026-02-30", 3000, "식비")
def test_month_boundary(self):
records = [
make_record("2026-10-01", 3000, "식비"),
make_record("2026-10-31", 2000, "교통"),
make_record("2026-11-01", 9000, "생활"),
]
self.assertEqual(monthly_total(records, "2026-10"), 5000)
def test_month_empty(self):
records = [
make_record("2026-11-01", 9000, "생활"),
]
self.assertEqual(monthly_total(records, "2026-10"), 0)
def test_budget_equal(self):
self.assertFalse(budget_check(5000, 5000))
def test_budget_over(self):
self.assertTrue(budget_check(5001, 5000))
return unittest.defaultTestLoader.loadTestsFromTestCase(LedgerChecks)
def run_checks(label, budget_check):
report = io.StringIO()
runner = unittest.TextTestRunner(stream=report, verbosity=0)
result = runner.run(make_suite(budget_check))
print(
f"{label}: 검사 {result.testsRun}개, "
f"실패 {len(result.failures)}개, "
f"오류 {len(result.errors)}개"
)
for test, _ in result.failures:
name = test.id().rsplit(".", 1)[-1]
print(f"실패 항목: {name}")
if result.errors:
print(report.getvalue(), end="")
return result
def main():
normal = run_checks("정상 규칙", is_over_budget)
def buggy_budget(total, limit):
return total >= limit
injected = run_checks("버그 주입", buggy_budget)
restored = run_checks("복구 확인", is_over_budget)
failed_names = [
test.id().rsplit(".", 1)[-1]
for test, _ in injected.failures
]
expected = (
normal.wasSuccessful()
and restored.wasSuccessful()
and injected.testsRun == 8
and failed_names == ["test_budget_equal"]
and not injected.errors
)
if not expected:
print("예상과 다른 검사 결과")
return 1
print("확인 목록 시연 완료")
return 0
if __name__ == "__main__":
raise SystemExit(main())
줄별 해설
import로 시작하는 부분은 이번 파일이 사용할 표준 라이브러리를 불러온다. io는 문자열을 출력받을 그릇을 제공하고, unittest는 검사를 실행하며, date는 실제로 존재하는 날짜인지 확인한다. CATEGORIES는 허용한 분류를 한곳에 모은 값이다.
make_record의 첫 두 줄은 날짜 문자열을 날짜 값으로 바꾼 뒤 다시 정해진 모양과 비교한다. 존재하지 않는 날짜이면 변환 과정에서 ValueError가 발생한다. 날짜 자체가 유효해도 YYYY-MM-DD 모양과 다르면 직접 ValueError를 발생시킨다.
type(amount) is not int는 금액이 정확히 정수 자료형인지 확인한다. 이어지는 조건은 0 이하를 거절한다. True는 참을 뜻하는 값이지만 Python에서는 일부 정수 연산에도 참여한다. 금액으로 받아들이지 않도록 여기서는 정확한 자료형을 확인한다. 반환하는 중괄호 묶음은 이름과 값을 짝지어 저장하는 딕셔너리다.
monthly_total은 각 기록의 날짜에서 앞 일곱 글자를 가져온다. YYYY-MM-DD 형식을 보장한 기록이므로 앞 일곱 글자는 연도와 월이다. 조회한 달과 같은 기록의 금액만 sum에 보내 합산한다. 해당 기록이 하나도 없으면 합계는 0이다.
is_over_budget은 비교 결과인 참 또는 거짓을 돌려준다. 표시 문구를 만들지 않고 판단만 돌려주므로 검사에서도 참과 거짓을 직접 비교할 수 있다. 이번에 일부러 바꿔 볼 곳은 이 함수의 비교 기호다.
make_suite는 검사할 예산 함수를 받아 새 검사 묶음을 만든다. 함수 안에 검사 클래스를 둔 이유는 전달받은 budget_check를 예산 검사에서 그대로 사용하기 위해서다. 클래스는 관련 동작을 한 이름 아래 묶는 틀이다. 여기서는 unittest.TestCase의 검사 기능을 받아 쓰는 틀로 이해하면 된다.
test_로 시작하는 여덟 함수가 검사 항목이다. self는 각 검사에서 unittest가 제공하는 비교 기능을 사용하는 통로다. assertEqual은 두 값이 같은지, assertFalse와 assertTrue는 각각 거짓과 참인지를 확인한다. assertRaises 안의 코드는 지정한 예외가 발생해야 통과한다.
subTest는 금액 검사 안의 여러 입력을 구분한다. 하나가 잘못되더라도 나머지 입력을 이어서 확인할 수 있다. 이 예제에서 testsRun에 표시되는 8은 test_ 함수 수다. 금액 입력이 네 개라고 해서 전체 검사 수가 11로 표시되지는 않는다.
run_checks는 새 검사 묶음을 실행하고 결과를 돌려준다. stream에 지정한 StringIO는 기본 보고서를 메모리에 받는다. 정상적인 시연에서는 그 보고서 전체를 출력하지 않으므로 실행 시간처럼 매번 달라지는 값이 나타나지 않는다. 예상하지 못한 검사 오류가 발생하면 원인을 읽을 수 있도록 보고서를 출력한다.
main은 같은 확인 목록에 정상 함수, 버그 함수, 정상 함수를 차례로 전달한다. 마지막 expected는 실패가 아무렇게나 한 개 나온 것이 아니라 정확히 test_budget_equal에서 발생했는지까지 확인한다. 마지막의 SystemExit는 main이 돌려준 0 또는 1을 프로그램의 종료 상태로 전달한다. 이 시연에서는 의도한 실패를 제대로 잡으면 전체 실행은 성공인 0으로 끝난다.
실행 결과
main.py가 있는 폴더에서 실행한다.
python3 main.py
정상 규칙: 검사 8개, 실패 0개, 오류 0개
버그 주입: 검사 8개, 실패 1개, 오류 0개
실패 항목: test_budget_equal
복구 확인: 검사 8개, 실패 0개, 오류 0개
확인 목록 시연 완료
두 번째 줄의 실패는 의도한 결과다. 반면 첫 번째나 네 번째 줄에서 실패가 나오면 수정이 필요하다. 이 파일은 버그 감지 시연을 포함하므로 이렇게 판정한다. 실제 가계부에서 정상 검사만 실행할 때는 모든 검사의 통과를 성공 기준으로 삼는다.
실무에서 자주 틀리는 것
기대 값을 같은 함수로 다시 구한다
아래처럼 같은 계산을 양쪽에 적으면 계산이 틀려도 두 값은 같다. 두 예시는 각각 독립적으로 실행할 수 있다. 잘못된 예시도 통과한다는 점이 문제다.
import unittest
def total(values):
return sum(values)
case = unittest.TestCase()
case.assertEqual(total([3000, 2000]), total([3000, 2000]))
기대 값은 규칙과 입력을 보고 직접 정한다.
import unittest
def total(values):
return sum(values)
case = unittest.TestCase()
case.assertEqual(total([3000, 2000]), 5000)
입력 오류를 숨겨서 확인할 수 없게 만든다
잘못된 금액을 조용히 0으로 바꾸면 입력 거절 규칙을 확인하기 어렵다. 아래 코드는 실행되지만 약속한 동작과 다르다.
def checked_amount(amount):
if type(amount) is not int or amount <= 0:
return 0
return amount
checked_amount(-1)
문제를 알리는 예외를 발생시키고 그 예외를 검사한다.
import unittest
def checked_amount(amount):
if type(amount) is not int or amount <= 0:
raise ValueError("금액은 양의 정수여야 한다")
return amount
case = unittest.TestCase()
with case.assertRaises(ValueError):
checked_amount(-1)
검사 결과를 받아 놓고 성공 문구만 출력한다
검사를 실행했다는 사실과 검사가 통과했다는 사실은 다르다. 다음 코드는 실제 결과를 읽지 않는다.
import io
import unittest
suite = unittest.TestSuite()
result = unittest.TextTestRunner(stream=io.StringIO()).run(suite)
print("검사 통과")
결과와 실행한 검사 수를 함께 확인한다. 빈 검사 묶음도 실패 없이 끝날 수 있으므로 검사 수를 놓치지 않는다.
import io
import unittest
suite = unittest.TestSuite()
result = unittest.TextTestRunner(stream=io.StringIO()).run(suite)
if result.testsRun > 0 and result.wasSuccessful():
print("검사 통과")
else:
print("검사 수 또는 결과 확인 필요")
완성 코드에서는 정상 실행의 검사 수를 화면에 표시하고 사람이 8개인지 확인한다. 의도한 버그 실행은 코드에서도 검사 수와 실패 항목을 확인한다.
한눈에 보기
| 항목 | 확인할 질문 | 이번 예제의 답 |
|---|---|---|
| 규칙 | 무엇이 맞는 결과인가 | 예산과 같으면 초과가 아니다 |
| 기대 값 | 실제 계산과 독립적으로 정했는가 | 월 합계를 5,000원으로 직접 지정 |
| 경계 | 규칙이 바뀌는 지점을 넣었는가 | 한도와 같음, 한도보다 1원 많음 |
| 감지 확인 | 일부러 틀리면 실패하는가 | 비교 기호 변경 시 한 항목 실패 |
| 복구 | 정상 규칙으로 다시 통과하는가 | 복구 확인에서 실패 0개 |
| 변경 검토 | 통과시키려고 기대 값을 바꾸지 않았는가 | 실행과 함께 변경 전후를 읽는다 |
검사 통과는 정해 둔 사례가 통과했다는 뜻이다. 가계부의 모든 동작을 확인했다는 뜻은 아니다. 새로운 규칙을 추가하거나 놓친 사례를 발견하면 확인 목록도 함께 늘린다. 다음에 코드의 구성을 정리할 때도 이 목록을 돌려 기존 결과가 유지되는지 확인할 수 있다.
연습 문제
- 예산 한도가 5,000원이고 합계가 4,999원일 때 초과가 아님을 확인하는 검사를 추가하라. 정상·버그 주입·복구 실행의 검사 수가 어떻게 바뀌는지 적어라.
- 2026년 10월의 기록이 있어도 2025년 10월 합계에는 포함되지 않는 검사를 추가하라. 월 숫자만 비교하는 코드로 바뀌었을 때 이 검사가 필요한 이유를 설명하라.
- 날짜 2026-10-1을 거절하는 검사를 추가하라. 달력에 있는 날인지 확인하는 것과 정해진 문자열 모양을 확인하는 것이 어떻게 다른지 설명하라.
정답과 해설
1. LedgerChecks 안에 test_budget_below라는 검사 함수를 추가하고 self.assertFalse(budget_check(4999, 5000))를 넣는다. 정상 함수와 버그 함수 모두 이 입력에서는 거짓을 돌려준다. 세 실행의 검사 수는 각각 9개가 되고, 실패 수는 계속 0개·1개·0개다. main의 injected.testsRun == 8도 9로 수정해야 한다. 이는 추가한 검사 수에 맞춘 변경이며, 예산의 기대 판단을 바꾸는 수정은 아니다.
2. test_month_year라는 검사 함수를 추가한다. records에 make_record("2026-10-01", 3000, "식비")로 만든 기록 하나를 넣고, self.assertEqual(monthly_total(records, "2025-10"), 0)으로 비교한다. 연도가 다르므로 합계는 0이다. 월 숫자만 비교하면 서로 다른 해의 10월 기록이 섞인다. 이 문제만 따로 적용하면 검사 수는 9개가 되므로 main의 검사 수 조건도 함께 고친다.
3. test_record_date_format이라는 검사 함수를 추가한다. with self.assertRaises(ValueError): 안에서 make_record("2026-10-1", 3000, "식비")를 호출한다. 달력의 날짜로는 10월 1일이 존재하지만 이 가계부는 일자를 두 자리로 적기로 했다. 완성 코드는 날짜 변환과 표준 모양 비교를 거쳐 해당 입력을 거절한다. 이 문제만 적용해도 검사 수는 9개다.
세 문제를 모두 적용하면 검사 수는 11개다. main의 검사 수 조건도 11로 바꾸고, 정상·버그 주입·복구의 실패 수는 그대로인지 실행한다. 추가된 검사와 검사 수 조건만 바뀌었는지 변경 전후도 확인한다. AI에게 수정을 맡겼더라도 마지막 확인은 실행 결과와 실제 변경 내용을 함께 읽는 일이다.