Devin.KR

테스트를 먼저 - 맞는다는 증거를 요구하기

개발자KR 조회 0

이 장에서 배우는 것

앞 장에서 받은 코드를 읽고 실행하며 변경 내용을 확인했다. 이번에는 코드가 오기 전에 “어떤 결과가 맞는가”부터 정한다. AI가 만든 프로그램이 끝까지 실행됐다는 사실과 사용자가 원한 동작을 했다는 사실은 다르다. 실행 중 예외가 없더라도 마감일 계산이 하루 어긋나거나, 이미 끝낸 일을 마감 임박 목록에 넣을 수 있다.

혼자 쓰는 메모·할 일 도구에 마감 임박 판정 기능을 붙인다고 하자. 이 기능은 작지만 날짜의 경계에 따라 결과가 바뀐다. Python 표준 라이브러리인 unittest로 기대 동작을 먼저 고정하고, AI에게 그 기대를 만족하는 구현을 요청한다. 완성 프로그램은 고정된 날짜를 사용해 테스트와 판정 예시를 함께 실행한다. 외부 패키지나 네트워크 연결은 필요하지 않다.

  • 요구사항을 입력과 기대 결과의 쌍으로 바꾼다.
  • unittest로 정상 사례와 날짜 경계의 동작을 검사한다.
  • 테스트를 먼저 작성하고 구현할 범위와 수정할 수 없는 범위를 AI에게 알린다.
  • 경계값과 반례를 추가해 그럴듯하지만 틀린 구현을 걸러 낸다.
  • 실행 성공, 테스트 통과, 요구 충족을 구분해 결과를 판단한다.

문제 상황

할 일 목록에 “마감이 가까운 일” 표시를 넣고 싶다. AI에게 “마감이 사흘 이내인 일을 골라 줘”라고 요청했더니 함수 하나를 받았다. 오늘이 10월 7일이면 10월 8일에 끝내야 하는 일이 표시된다. 얼핏 보면 잘 작동한다.

그런데 며칠 뒤 살펴보니 어제 마감한 일도 표시된다. 날짜 차이가 3 이하인지 검사했기 때문이다. 어제 마감한 일의 날짜 차이는 음수이므로 이 조건을 만족한다. 또 다른 구현은 날짜 차이가 3보다 작은지만 검사해 정확히 사흘 뒤인 일을 놓친다. 두 구현 모두 문법 오류 없이 실행되지만, 사용자가 생각한 “사흘 이내”와 다르다.

이 문제를 막으려면 먼저 말의 빈틈을 줄여야 한다. 이 도구에서는 마감 임박을 다음처럼 정의한다. 완료하지 않은 일이며, 오늘부터 마감일까지의 날짜 차이가 0 이상이고 지정한 기간 이하이면 마감 임박이다. 오늘 마감하는 일과 기간의 마지막 날에 마감하는 일은 포함한다. 마감이 지난 일은 다른 표시가 필요한 대상으로 보고 이번 판정에서는 제외한다.

이 정의가 모든 도구에 맞는 것은 아니다. 지난 일을 함께 보여 주고 싶은 사람도 있다. 중요한 것은 선택한 규칙을 코드보다 먼저 드러내는 일이다. AI가 “마감 임박”의 뜻을 알아서 결정하게 두면, 나중에 결과가 달라도 어디서 판단이 달라졌는지 찾기 어렵다.

오늘이 2026년 10월 7일이고 기준 기간이 3일일 때의 기대 동작
상황마감일완료 여부기대 결과
오늘 마감2026-10-07미완료True
기간 마지막 날2026-10-10미완료True
기간을 하루 벗어남2026-10-11미완료False
이미 마감이 지남2026-10-06미완료False
날짜는 가까우나 완료함2026-10-08완료False

이 표는 구현 방법을 지시하지 않는다. 어떤 입력에서 어떤 답을 내야 하는지를 정한다. 날짜 차이를 어떤 변수에 담을지, 조건문을 어떻게 배치할지는 구현 단계에서 정할 수 있다. 먼저 고정할 것은 사용자가 확인할 수 있는 결과다.

기대 동작을 실행 가능한 검사로 바꾸기

테스트(test)는 입력을 넣고 실제 결과를 기대 결과와 비교하는 검사다. 여기서는 작은 함수 하나를 따로 검사한다. 이런 검사를 단위 테스트(unit test)라고 한다. 할 일 목록 전체를 저장하거나 화면을 만들기 전에 날짜 판정 규칙부터 확인할 수 있다.

unittest는 검사들을 묶어 실행하고 실패한 검사를 기록하는 표준 라이브러리다. 테스트 클래스는 관련 검사를 담는 그릇이다. 클래스 문법을 아직 익숙하게 읽지 못하더라도 이번 예제에서는 정해진 틀로 받아들여도 된다. unittest.TestCase를 물려받은 클래스 안에 test_로 시작하는 함수를 두면, 테스트를 찾아 실행하는 도구가 그 함수들을 검사로 모은다. 각 검사 함수의 self는 현재 검사에 필요한 비교 도구를 사용하는 통로다.

assertTrue는 결과가 참인지 확인하고, assertFalse는 거짓인지 확인한다. assertRaises는 지정한 예외가 발생하는지 확인한다. 여기서 단언(assertion)이란 “이 조건이 맞아야 한다”는 요구를 실행 중 확인하는 표현이다. 조건이 맞지 않으면 검사에 실패했다고 기록한다.

테스트에는 기대 결과를 직접 적는다. 오늘 마감한 미완료 일을 검사할 때는 결과가 True여야 한다고 적는다. 기대 결과까지 판정 함수로 계산하면 같은 오류를 두 번 실행하는 셈이다. 테스트는 구현이 내놓은 답을 되풀이하는 문서가 아니라, 구현과 독립적으로 정한 약속이어야 한다.

같은 입력에 대해 요구사항에서 정한 기대 결과와 구현의 실제 결과를 비교해야 한다

날짜도 외부 상태에 맡기지 않는다. 함수 안에서 오늘 날짜를 직접 읽으면, 같은 테스트라도 실행 날짜에 따라 답이 달라질 수 있다. 이번 함수는 오늘을 뜻하는 today를 인수로 받는다. 테스트에서는 2026년 10월 7일을 전달한다. 실제 도구에 연결할 때 오늘 날짜를 어디서 얻을지는 별도로 정할 수 있지만, 판정 규칙을 검사하는 동안에는 날짜를 고정한다.

이 함수가 받는 값의 범위도 작게 정한다. 마감일과 오늘은 date 값이고, 완료 여부는 True 또는 False다. 기준 기간은 0 이상의 정수다. 모든 입력 오류를 처리하는 도구를 만드는 것이 목적은 아니다. 다만 기간이 음수이거나 정수가 아닌 경우에는 예외를 내도록 요구한다. 기간 0은 오류가 아니라 “오늘 마감하는 일만 확인한다”는 뜻이다.

테스트를 먼저 쓰고 구현을 요청하기

테스트를 먼저 쓴다는 말은 아직 없는 함수가 어떻게 동작해야 하는지부터 코드로 적는다는 뜻이다. 시작할 때는 함수 몸체에 NotImplementedError를 발생시키는 한 줄을 둘 수 있다. 이 예외는 아직 구현하지 않았음을 알린다. 그 상태에서 정상 결과를 요구하는 검사가 실패하는 것은 자연스럽다. 함수가 없는 상태의 오류와, 구현은 있지만 답이 틀린 상태의 실패를 구분해 읽는다.

순서는 간단하다. 요구사항을 표로 적고, 표를 테스트로 옮긴다. 구현 전 검사를 실행해 아직 요구를 만족하지 못한다는 사실을 확인한다. 그다음 함수 본체만 구현해 달라고 요청한다. 구현을 받은 뒤에는 다시 실행하고 변경 전후의 차이인 diff도 확인한다. 테스트가 그대로인지, 함수 밖에 불필요한 변경이 생겼는지를 함께 본다.

AI에게는 “테스트를 통과하게 해 줘”라는 말만 보내지 않는다. 무엇을 바꿔도 되는지까지 알려야 한다. 다음 요청은 테스트와 구현의 역할을 분리한다.

할 일 도구의 is_due_soon 함수를 구현한다. 아래 테스트를 기대 동작의 기준으로 삼는다. 미완료인 일 중 오늘부터 days일 뒤까지의 마감만 포함한다. 양끝을 포함하고 지난 마감은 제외한다. days는 0 이상의 정수이며, 잘못된 자료형에는 TypeError, 음수에는 ValueError를 발생시킨다.

기존 테스트의 입력, 기대 결과, 이름, 개수를 바꾸지 않는다. 테스트를 삭제하거나 건너뛰지 않는다. is_due_soon 함수 본체만 수정한다. 테스트와 요구사항이 모순된다고 판단하면 구현을 바꾸기 전에 해당 입력과 이유를 설명한다. 테스트 실행 결과와 수정한 부분을 보고한다.

이 요청은 테스트도 틀릴 수 있다는 가능성을 남겨 둔다. 사람이 날짜를 잘못 적었을 수도 있다. 하지만 테스트가 실패했다는 이유만으로 AI가 기대 결과를 바꾸도록 허용하지는 않는다. 요구를 다시 읽고 사람이 변경을 결정한 뒤에만 테스트를 수정한다. 그때는 어떤 약속이 바뀌었는지 기록한다.

AI의 답에 “모두 통과했다”라고 적혀 있어도 그대로 믿지 않는다. 실행할 수 있는 환경에서 실제로 실행했는지 확인한다. 실행하지 못했다면 “코드를 검토했지만 테스트는 실행하지 못했다”는 보고가 맞다. 독자는 자신이 받은 파일을 다시 실행해야 한다. 보고된 결과와 현재 파일의 결과가 같다는 보장은 별도로 확인해야 한다.

테스트를 먼저 쓰면 구현 방향이 좁아진다. 그렇다고 몇 개의 검사가 모든 오류를 찾아 준다는 뜻은 아니다. 처음 적은 요구와 테스트가 빠뜨린 상황은 여전히 남는다. 따라서 구현 요청 뒤에도 경계와 반례를 검토하되, 기존 기대를 느슨하게 바꾸는 방식으로 검사 수를 늘리지 않는다.

경계값과 반례로 검사 범위를 넓히기

경계값(boundary value)은 결과가 바뀌는 지점 주변의 값이다. 기준 기간이 3일이면 날짜 차이가 -1, 0, 3, 4인 사례가 중요하다. 1일 뒤의 사례만 검사하면 “3보다 작다”와 “3 이하이다”를 구분할 수 없다. 결과가 바뀌는 선의 양쪽을 확인해야 조건의 방향과 포함 여부를 검사할 수 있다.

반례(counterexample)는 어떤 설명이나 구현이 틀렸음을 보여 주는 사례다. “날짜 차이가 3 이하면 된다”는 구현에는 어제 마감한 일이 반례다. “마감일의 일 숫자에서 오늘의 일 숫자를 빼면 된다”는 구현에는 연말을 넘어가는 날짜가 반례다. 날짜를 단순한 숫자 조각으로 다루면 달과 해가 바뀔 때 뜻이 달라진다.

기존 테스트는 유지한다. 이 요구에서 결과가 바뀌는 경계를 찾아 추가 검사 후보를 제안한다. 특히 지난 마감, 기간 마지막 날, 완료한 일, 연도 변경, 윤년, 기간 0을 살펴본다. 각 후보에 입력과 기대 결과를 적고, 어떤 잘못된 구현을 걸러 내는지 설명한다. 구현을 호출해서 기대 결과를 계산하지 않는다.

추가 후보도 사람이 검토한다. AI가 많은 사례를 제시했다고 해서 모두 유용한 것은 아니다. 같은 이유로 참이 되는 날짜만 여러 개 넣으면 검사 수는 늘지만 새로운 오류를 구분하는 힘은 거의 늘지 않는다. 이번 묶음에는 연도 변경과 윤년을 넣어 날짜 전체의 차이를 계산하는지 확인한다. 기간의 자료형과 음수 처리도 별도 검사로 둔다.

마감 임박 판정은 날짜 차이 0과 3을 포함하고 그 바깥을 제외한다

실행 결과를 읽을 때도 층위를 구분한다. 프로그램 종료는 코드가 끝까지 진행됐다는 뜻이다. 테스트 통과는 실행한 검사들의 기대 결과와 일치했다는 뜻이다. 요구 충족을 판단하려면 그 검사들이 중요한 요구를 실제로 다뤘는지까지 살펴야 한다. 세 가지를 한 문장으로 뭉뚱그려 “잘 된다”라고 말하면 빠진 조건을 놓치기 쉽다.

공식 문서가 필요하면 unittest의 검사 도구 설명과 날짜 자료형 설명을 확인할 수 있다. 문서는 도구의 동작을 확인하는 근거다. 우리 도구에서 지난 마감을 포함할지 같은 업무 규칙은 문서가 대신 결정하지 않는다.

완성 코드

다음 전체 코드를 main.py로 저장한다. Python 3.12 이상에서 표준 라이브러리만으로 실행된다. 검사 10개를 먼저 실행하고, 모두 통과하면 네 가지 할 일의 판정을 출력한다. 날짜와 출력 순서를 고정했으므로 성공할 때의 출력은 실행 시점에 따라 달라지지 않는다.

import io
import unittest
from datetime import date


def is_due_soon(due_date, today, completed=False, days=3):
    if type(days) is not int:
        raise TypeError("days는 정수여야 한다")
    if days < 0:
        raise ValueError("days는 0 이상이어야 한다")
    if completed:
        return False

    remaining = (due_date - today).days
    return 0 <= remaining <= days


class DueSoonTests(unittest.TestCase):
    def setUp(self):
        self.today = date(2026, 10, 7)

    def test_today_is_included(self):
        self.assertTrue(
            is_due_soon(date(2026, 10, 7), self.today)
        )

    def test_last_day_is_included(self):
        self.assertTrue(
            is_due_soon(date(2026, 10, 10), self.today)
        )

    def test_day_after_limit_is_excluded(self):
        self.assertFalse(
            is_due_soon(date(2026, 10, 11), self.today)
        )

    def test_overdue_is_excluded(self):
        self.assertFalse(
            is_due_soon(date(2026, 10, 6), self.today)
        )

    def test_completed_is_excluded(self):
        self.assertFalse(
            is_due_soon(
                date(2026, 10, 8), self.today, completed=True
            )
        )

    def test_crossing_year(self):
        self.assertTrue(
            is_due_soon(
                date(2027, 1, 2), date(2026, 12, 30), days=3
            )
        )

    def test_leap_day_is_counted(self):
        self.assertTrue(
            is_due_soon(
                date(2028, 3, 1), date(2028, 2, 28), days=2
            )
        )

    def test_zero_days_means_today_only(self):
        self.assertTrue(
            is_due_soon(date(2026, 10, 7), self.today, days=0)
        )
        self.assertFalse(
            is_due_soon(date(2026, 10, 8), self.today, days=0)
        )

    def test_negative_days_is_rejected(self):
        with self.assertRaises(ValueError):
            is_due_soon(date(2026, 10, 8), self.today, days=-1)

    def test_non_integer_days_is_rejected(self):
        for invalid_days in (3.0, "3", True):
            with self.subTest(days=invalid_days):
                with self.assertRaises(TypeError):
                    is_due_soon(
                        date(2026, 10, 8),
                        self.today,
                        days=invalid_days,
                    )


def main():
    suite = unittest.defaultTestLoader.loadTestsFromTestCase(
        DueSoonTests
    )
    report = io.StringIO()
    runner = unittest.TextTestRunner(
        stream=report, verbosity=0
    )
    result = runner.run(suite)

    print(
        f"테스트: {result.testsRun}개, "
        f"실패: {len(result.failures)}개, "
        f"오류: {len(result.errors)}개"
    )
    if not result.wasSuccessful():
        print(report.getvalue(), end="")
        return 1

    print("테스트 결과: 통과")
    today = date(2026, 10, 7)
    tasks = [
        ("초안 정리", date(2026, 10, 7), False),
        ("자료 확인", date(2026, 10, 10), False),
        ("지난 메모 정리", date(2026, 10, 6), False),
        ("완료한 작업", date(2026, 10, 8), True),
    ]

    print(f"판정 기준: 오늘 {today.isoformat()}, 기간 3일")
    for title, due_date, completed in tasks:
        soon = is_due_soon(due_date, today, completed)
        label = "마감 임박" if soon else "대상 아님"
        print(f"{title}: {label}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

줄별 해설

처음 세 줄은 출력 기록을 담는 io, 검사를 실행하는 unittest, 날짜를 만드는 date를 가져온다. date에는 연도·월·일을 전달한다. 시각이나 시간대는 이번 규칙에 필요하지 않으므로 날짜만 다룬다.

is_due_soon의 네 인수는 마감일, 오늘, 완료 여부, 기준 기간이다. completed와 days에는 기본값이 있으므로 미완료인 일과 3일 기준을 검사할 때는 날짜 두 개만 전달해도 된다. 함수가 오늘을 직접 읽지 않는 점이 재현 가능한 검사의 출발점이다.

type(days) is not int는 기준 기간이 정수인지 검사한다. Python에서는 True와 False도 일부 정수 연산에 참여할 수 있지만, 이 함수에서는 완료 여부를 기간에 잘못 넣는 일을 허용하지 않는다. 그래서 정확히 int인 값만 받는다. 3.0처럼 숫자상으로 정수와 같은 값도 이번 입력 약속에서는 제외한다.

음수 검사와 완료 여부 검사의 순서에도 뜻이 있다. 기간이 잘못됐다면 완료한 일에서도 먼저 예외를 발생시킨다. 잘못된 설정을 완료 여부로 숨기지 않으려는 선택이다. 그다음 완료한 일은 False로 돌려보낸다. 날짜를 계산할 필요가 없는 경우다.

두 date 값을 빼면 날짜 사이의 간격을 나타내는 값이 나온다. 그 값의 days가 날짜 차이다. 0 <= remaining <= days는 날짜 차이가 두 경계 사이에 있는지 검사한다. 오늘은 0이고, 어제는 -1이다. 두 곳에 등호가 있으므로 오늘과 기간 마지막 날을 포함한다.

DueSoonTests는 판정 규칙의 검사 묶음이다. setUp은 각 검사 직전에 실행돼 self.today를 설정한다. 각 검사가 같은 오늘을 사용하게 하면서도 검사끼리 실행 순서에 의존하지 않게 한다. 함수 이름은 검사 의도를 나타내므로 실패 보고에서 어느 약속이 어긋났는지 읽는 데 도움이 된다.

첫 다섯 검사는 기대 동작 표를 코드로 옮긴 것이다. 연도 변경 검사는 12월 30일부터 1월 2일까지가 사흘임을 확인한다. 윤년 검사는 2028년 2월 29일을 포함해 2월 28일부터 3월 1일까지가 이틀임을 확인한다. 월이나 일 숫자만 따로 빼는 구현은 이런 사례를 제대로 처리하지 못한다.

기간 0 검사는 참과 거짓을 함께 확인한다. 오늘만 포함한다는 규칙은 오늘이 참이라는 검사 하나로는 부족하다. 내일은 거짓이어야 범위가 오늘에서 끝난다는 사실을 확인할 수 있다. 테스트 함수 하나에 단언이 둘 있어도 테스트 실행 개수는 하나로 센다.

assertRaises를 사용하는 with 문은 그 안의 호출이 지정한 예외를 내는지 검사한다. 예외가 없거나 다른 예외가 나면 통과하지 못한다. subTest는 반복문 안의 입력들을 구분해 보고하는 작은 검사 구획이다. 3.0, 문자열 "3", True를 같은 요구로 검사하되, 문제가 생긴 입력을 알 수 있게 한다. 이 검사 함수 역시 전체 개수에서는 하나로 센다.

main은 테스트 묶음을 모아 실행한다. StringIO는 문자열을 담는 메모리 안의 출력 공간이다. 기본 테스트 보고에는 실행 시간이 포함되므로 성공 보고를 그곳에 담고, 화면에는 검사 개수와 실패·오류 개수만 직접 출력한다. 이 방식은 성공 출력의 시간을 고정하려는 것이며, 검사 자체를 생략하지 않는다.

실패는 비교한 결과가 기대와 다르다는 뜻이다. 오류는 검사 도중 예상하지 않은 예외가 발생하는 등 검사를 정상적으로 마치지 못했다는 뜻이다. 어느 쪽이든 발생하면 저장한 상세 보고를 출력하고 1을 반환한다. 모두 통과했을 때만 판정 예시를 실행하고 0을 반환한다. 마지막 SystemExit는 이 반환값을 프로그램의 종료 상태로 전달한다.

실행 결과

main.py가 있는 디렉터리에서 다음 명령을 실행한다.

python3 main.py

예상 출력은 다음과 같다. 검사 함수는 10개이며, 함수 안의 여러 비교나 작은 검사 구획을 별도 함수처럼 세지 않는다.

테스트: 10개, 실패: 0개, 오류: 0개
테스트 결과: 통과
판정 기준: 오늘 2026-10-07, 기간 3일
초안 정리: 마감 임박
자료 확인: 마감 임박
지난 메모 정리: 대상 아님
완료한 작업: 대상 아님

이 출력은 이번에 적은 검사와 구현이 일치한다는 증거다. 저장 기능이나 화면 표시까지 검증했다는 뜻은 아니다. 판정 예시는 실제 함수의 결과를 출력하지만, 예시를 눈으로 읽는 일만으로 테스트를 대신하지 않는다. 기대 결과를 비교하는 검사와 사람이 읽기 쉬운 예시는 서로 다른 역할을 맡는다.

실무에서 자주 틀리는 것

지난 마감도 가까운 마감으로 처리한다

상한만 검사하면 음수가 모두 포함된다. 다음 틀린 예시는 어제 마감한 일을 True로 출력한다. 각각의 짧은 예시는 완성 코드와 별개로 실행할 수 있다.

from datetime import date

today = date(2026, 10, 7)
due_date = date(2026, 10, 6)
remaining = (due_date - today).days
print(remaining <= 3)

고친 예시는 하한 0을 함께 검사하므로 False를 출력한다. 이미 지난 일을 별도 기능에서 다루더라도, 이번 함수의 약속은 그대로 유지한다.

from datetime import date

today = date(2026, 10, 7)
due_date = date(2026, 10, 6)
remaining = (due_date - today).days
print(0 <= remaining <= 3)

마지막 날의 등호를 빠뜨린다

사흘 이내에 사흘 뒤를 포함하기로 했는데 <를 쓰면 마지막 날이 빠진다. 다음 틀린 예시는 False를 출력한다.

from datetime import date

remaining = (date(2026, 10, 10) - date(2026, 10, 7)).days
print(0 <= remaining < 3)

고친 예시는 True를 출력한다. 기간 안쪽의 사례만 확인하면 이 차이를 놓치므로, 마지막 날과 그다음 날을 함께 검사한다.

from datetime import date

remaining = (date(2026, 10, 10) - date(2026, 10, 7)).days
print(0 <= remaining <= 3)

실제 결과를 기대 결과로 복사한다

다음 틀린 검사는 실제 결과를 expected에 다시 담는다. 판정이 틀렸는데도 자기 자신과 비교하므로 통과한다. 이 코드를 실행하면 그 문제가 드러난다.

import unittest


class CopiedAnswerTests(unittest.TestCase):
    def test_last_day(self):
        actual = 3 < 3
        expected = actual
        self.assertEqual(actual, expected)


case = CopiedAnswerTests("test_last_day")
result = unittest.TestResult()
case.run(result)
print(result.wasSuccessful())

고친 검사는 요구사항에서 정한 True와 비교한다. 같은 잘못된 계산을 검사하면 이번에는 False가 출력된다. 기대 결과를 고치는 대신 구현의 등호를 고쳐야 하는 상황이다.

import unittest


class IndependentAnswerTests(unittest.TestCase):
    def test_last_day(self):
        actual = 3 < 3
        self.assertEqual(actual, True)


case = IndependentAnswerTests("test_last_day")
result = unittest.TestResult()
case.run(result)
print(result.wasSuccessful())

실패한 테스트를 지우거나 건너뛰는 일도 같은 문제를 만든다. 검사 결과만 보기 좋게 만들면 요구를 만족했다는 증거가 사라진다. 테스트 변경이 필요하다면 원래 요구가 무엇이었고 왜 바뀌는지 먼저 설명한 뒤, 변경된 입력과 기대 결과를 다시 검토한다.

한눈에 보기

테스트를 먼저 사용하는 작업에서 확인할 기준
단계남길 것확인할 질문
요구 정리입력과 기대 결과 표오늘·지난 마감·마지막 날의 뜻이 정해졌는가
테스트 작성구현과 독립적인 비교틀린 구현에서도 통과하는 검사가 아닌가
구현 요청수정 범위와 테스트 보존 규칙실패를 없애려고 기대 결과를 바꾸지 않았는가
범위 보강경계값과 반례각 사례가 다른 종류의 오류를 구분하는가
검증실행 결과와 변경 내용 확인현재 파일을 실행했고 검사 누락도 살폈는가

검사를 많이 만드는 것보다 중요한 것은 요구와 검사의 연결이다. 어떤 검사가 어떤 약속을 지키는지 설명할 수 있어야 한다. 그 연결이 분명하면 AI가 코드를 바꾼 뒤에도 무엇을 다시 확인해야 할지 알 수 있다.

연습 문제

  1. 오늘이 2026년 10월 7일이고 기간이 3일이다. 날짜 차이가 -1, 0, 3, 4인 미완료 일의 기대 결과를 적는다. 각 값이 어떤 조건 오류를 드러내는지도 설명한다.
  2. 완료한 일이 오늘 마감하더라도 False여야 한다. 완성 코드의 검사 클래스에 이 요구를 확인하는 검사 함수를 추가한다. 구현이나 기존 테스트는 바꾸지 않는다.
  3. AI가 “마지막 날 검사만 실패하므로 그 검사의 기대 결과를 False로 바꿨다”라고 보고했다. 어떤 내용을 다시 요청해야 하는지 짧은 요청문을 작성한다.
  4. 완성 코드에 기간 1 검사를 추가한다. 내일 마감은 True, 모레 마감은 False여야 한다. 검사를 추가한 뒤 테스트 개수와 실패·오류 개수의 예상 출력도 적는다.

정답과 해설

첫 문제의 결과는 순서대로 False, True, True, False다. -1은 하한 검사가 빠진 구현을 드러낸다. 0은 오늘을 포함하는지 확인한다. 3은 마지막 날의 등호를 확인한다. 4는 허용 기간을 넘어선 날이 제외되는지 확인한다. 모두 미완료라는 조건을 유지해야 날짜 범위만 비교할 수 있다.

두 번째 문제에서는 다음 검사 함수를 DueSoonTests 안에 추가한다. 들여쓰기를 포함한 클래스 전체를 아래처럼 읽을 수 있다. 기존 클래스에 옮길 때는 같은 이름의 별도 클래스를 만들지 않고 검사 함수만 추가한다.

import unittest
from datetime import date


def is_due_soon(due_date, today, completed=False, days=3):
    if type(days) is not int:
        raise TypeError("days는 정수여야 한다")
    if days < 0:
        raise ValueError("days는 0 이상이어야 한다")
    if completed:
        return False
    return 0 <= (due_date - today).days <= days


class CompletedTodayTests(unittest.TestCase):
    def test_completed_today_is_excluded(self):
        self.assertFalse(
            is_due_soon(
                date(2026, 10, 7),
                date(2026, 10, 7),
                completed=True,
            )
        )


suite = unittest.defaultTestLoader.loadTestsFromTestCase(
    CompletedTodayTests
)
result = unittest.TestResult()
suite.run(result)
print(result.wasSuccessful())

이 독립 예시를 실행하면 True가 출력된다. 완성 코드의 클래스에 검사 함수 하나를 추가했다면 전체 검사 개수는 11개가 된다. 원래 완료 검사와 비교하면 날짜가 경계인 오늘로 바뀌었다. 완료 여부가 날짜 포함 조건보다 우선하는지 확인하는 사례다.

세 번째 문제의 요청문은 다음과 같이 쓸 수 있다.

요구사항은 기간 마지막 날을 포함한다. 변경한 테스트의 기대 결과를 원래 True로 되돌린다. 기존 테스트를 유지하고 구현의 날짜 비교 조건을 수정한다. 수정 후 전체 테스트를 실행하고, 테스트를 바꾸지 않았다는 것을 변경 내용과 함께 보고한다.

되돌릴 기준은 AI의 새 출력이 아니라 이미 합의한 요구다. 만약 마지막 날을 제외하도록 도구의 뜻을 바꾸려는 것이라면, 먼저 요구 변경을 결정하고 표와 테스트를 함께 수정해야 한다. 통과를 얻기 위한 수정과 요구 변경에 따른 수정은 이유가 다르다.

네 번째 문제는 다음 검사 함수 하나를 기존 DueSoonTests 안에 추가하면 된다. 여기서는 실행 가능한 독립 예시로도 제시한다.

import unittest
from datetime import date


def is_due_soon(due_date, today, completed=False, days=3):
    if type(days) is not int:
        raise TypeError("days는 정수여야 한다")
    if days < 0:
        raise ValueError("days는 0 이상이어야 한다")
    if completed:
        return False
    return 0 <= (due_date - today).days <= days


class OneDayTests(unittest.TestCase):
    def test_one_day_limit(self):
        today = date(2026, 10, 7)
        self.assertTrue(
            is_due_soon(date(2026, 10, 8), today, days=1)
        )
        self.assertFalse(
            is_due_soon(date(2026, 10, 9), today, days=1)
        )


suite = unittest.defaultTestLoader.loadTestsFromTestCase(
    OneDayTests
)
result = unittest.TestResult()
suite.run(result)
print(result.wasSuccessful())

독립 예시의 출력은 True다. 원래 완성 코드에 이 검사 함수만 추가하면 요약은 “테스트: 11개, 실패: 0개, 오류: 0개”가 된다. 두 번째 문제의 검사도 함께 추가했다면 12개다. 두 비교를 한 함수에 넣었으므로 이 문제에서 증가한 검사 개수는 하나다.

테스트가 실패하면 곧바로 코드를 여러 곳 바꾸기보다 실패한 입력과 기대 결과를 먼저 읽는다. 다음 장에서는 이 증거를 출발점으로 삼아 같은 문제를 다시 만들고, 원인 후보를 세우고, 확인하는 디버깅 과정을 다룬다.

댓글 0

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

댓글을 남기려면 로그인이 필요합니다.