포스트

자동 생성 테스트를 신뢰하기 전에 확인할 체크리스트

AI와 테스트 생성 도구가 만든 테스트가 단순히 통과하는 데서 그치지 않고 결함을 실제로 잡는지, 구현 복제·경계값·독립성·결정성·실패 검증 다섯 축으로 확인한다.

자동 생성 테스트를 신뢰하기 전에 확인할 체크리스트

AI 코딩 에이전트나 테스트 생성 도구에 “이 함수 테스트를 만들어 줘”라고 하면 몇 초 만에 테스트 파일이 생긴다. 테스트 러너도 초록색이다. 하지만 통과하는 테스트결함을 잡는 테스트는 다르다.

자동 생성 테스트를 검토할 때 핵심 질문은 하나다.

구현이 잘못되었을 때 이 테스트가 실제로 실패하는가?

아래 체크리스트는 프레임워크와 언어에 관계없이 이 질문을 확인하는 순서다.

1. 기대값이 구현을 그대로 복제하지 않는가

가장 흔한 실패는 테스트가 운영 코드와 같은 계산을 반복하는 것이다.

1
2
3
int calculateDiscount(int price, int rate) {
    return price - (price * rate / 100);
}

다음 테스트는 얼핏 그럴듯하지만 기대값을 같은 공식으로 계산한다.

1
2
3
4
5
6
7
8
@Test
void calculatesDiscount() {
    int price = 10_000;
    int rate = 20;
    int expected = price - (price * rate / 100);

    assertEquals(expected, calculateDiscount(price, rate));
}

구현과 테스트가 같은 오해를 공유하면 둘 다 틀린 채 통과한다. 기대값은 요구사항에서 독립적으로 도출해 구체적인 값으로 적는 편이 낫다.

1
2
3
4
@Test
void appliesTwentyPercentDiscount() {
    assertEquals(8_000, calculateDiscount(10_000, 20));
}

검토할 때는 기대값이 어디서 왔는지 확인한다. 운영 코드의 식을 복사했거나 운영 메서드를 호출해 기대값을 만들었다면 테스트의 독립성이 약하다.

2. 정상 경로만 확인하지 않는가

생성 도구는 함수 이름과 현재 구현에서 가장 분명한 정상 입력을 먼저 고른다. 그래서 테스트 수는 많아도 입력의 의미는 비슷할 수 있다.

최소한 다음 범주를 확인한다.

범주확인할 입력
정상값대표적인 일반 입력
경계값0, 1, 최댓값, 범위의 양 끝
빈 값빈 문자열, 빈 컬렉션, 값 없음
잘못된 값음수, 형식 오류, 허용 범위 밖
중복·순서중복 원소, 정렬되지 않은 입력

모든 함수에 모든 범주를 억지로 넣을 필요는 없다. 요구사항의 분기와 데이터 경계가 테스트 입력에 드러나는지가 중요하다. 예를 들어 할인율이 0~100이라면 20 하나만 검사하지 말고 적어도 0, 100, 범위 밖 값의 정책을 확인한다.

3. 실패의 종류까지 검증하는가

예외가 발생했다는 사실만 확인하면 엉뚱한 원인으로 실패해도 테스트가 통과할 수 있다.

1
assertThrows(RuntimeException.class, () -> parseOrder(input));

가능하면 계약에 맞는 구체적인 예외 타입과 외부에 보장하는 오류 정보를 검증한다.

1
2
3
4
5
6
IllegalArgumentException error = assertThrows(
    IllegalArgumentException.class,
    () -> parseOrder("missing-price")
);

assertEquals("price is required", error.getMessage());

다만 내부 구현 문구까지 계약이 아닌데 메시지 전체를 고정하면 사소한 표현 변경에도 테스트가 깨진다. 사용자가 의존하는 오류 코드·타입·필드만 검증한다.

4. 테스트끼리 독립적인가

자동 생성된 여러 테스트가 같은 전역 상태, 데이터베이스 레코드, 임시 파일을 공유하면 실행 순서에 따라 결과가 바뀐다.

다음을 확인한다.

  • 각 테스트가 필요한 상태를 직접 준비하는가
  • 테스트가 만든 파일·레코드·환경 변수를 정리하는가
  • 단독 실행과 전체 실행 결과가 같은가
  • 테스트 순서를 바꿔도 통과하는가
  • 병렬 실행에서도 같은 자원을 충돌 없이 쓰는가

공통 준비 코드를 쓰는 것 자체가 문제는 아니다. 한 테스트의 변경된 상태가 다음 테스트에 남는 것이 문제다. C++ 테스트로 치면 전역 fixture를 여러 테스트가 수정하면서 초기화 시점에 기대는 상황과 같다.

5. 시간·난수·네트워크에 흔들리지 않는가

같은 코드에서 결과가 간헐적으로 바뀌는 테스트를 flaky test라고 한다. 자동 생성 테스트는 외부 의존성을 그대로 호출하거나 현재 시각을 직접 읽는 코드를 쉽게 만든다.

1
assertEquals(LocalDate.now(), service.expirationDate());

자정 경계나 시간대가 개입하면 실패할 수 있다. 시계가 주입 가능한 코드라면 테스트에서 고정한다.

1
2
3
4
Clock clock = Clock.fixed(
    Instant.parse("2026-08-12T00:00:00Z"),
    ZoneOffset.UTC
);

난수는 seed를 고정하고, 네트워크는 단위 테스트에서 제어 가능한 대역으로 바꾼다. 실제 서비스와의 연결은 별도의 통합 테스트로 분리해 실패 원인을 구분한다.

가장 빠른 검증: 구현을 일부러 망가뜨려 본다

리뷰만으로 애매하면 운영 코드에 작은 결함을 임시로 넣고 테스트를 실행한다.

  • 비교 연산자 >>=로 바꾼다.
  • 반환값에 +1을 한다.
  • 조건문 한 분기를 제거한다.
  • 예외를 발생시키지 않게 한다.

관련 테스트가 실패하지 않는다면 그 테스트는 해당 동작을 보호하지 못한다. 검증 후 변경은 반드시 되돌린다.

이 방식을 체계화한 것이 mutation testing(변이 테스트)이다. 도구가 운영 코드에 작은 변이(mutant)를 만들고, 기존 테스트가 그 변이를 잡아내는지 측정한다. 코드 커버리지가 “코드를 실행했는가”를 본다면 변이 테스트는 “잘못된 코드를 구별했는가”를 본다.

생성 직후 실행할 최소 절차

자동 생성 테스트를 받으면 다음 순서로 확인한다.

  1. 테스트만 실행해 현재 상태에서 통과하는지 본다.
  2. 테스트 이름과 입력이 요구사항의 어떤 사례를 표현하는지 읽는다.
  3. 정상값·경계값·실패 입력이 구분되어 있는지 확인한다.
  4. 기대값이 운영 코드와 독립적으로 계산되었는지 확인한다.
  5. 대상 구현에 작은 결함을 넣었을 때 관련 테스트가 실패하는지 본다.
  6. 전체 테스트를 반복 또는 병렬 실행해 간헐 실패가 없는지 확인한다.

체크리스트

  • 테스트 이름만 읽어도 보호하는 동작을 알 수 있다.
  • 기대값을 운영 코드와 같은 로직으로 계산하지 않는다.
  • 요구사항의 경계와 실패 경로를 포함한다.
  • 너무 넓은 예외 타입이나 의미 없는 assertion을 쓰지 않는다.
  • 테스트 간 실행 순서와 공유 상태에 의존하지 않는다.
  • 시간·난수·네트워크를 제어한다.
  • 구현을 잘못 바꾸면 관련 테스트가 실패한다.

자동 생성은 테스트의 초안 작성 비용을 줄인다. 테스트가 무엇을 보장하는지 결정하고, 틀린 구현을 실제로 거부하는지 확인하는 일까지 자동으로 끝내 주지는 않는다. 생성된 줄 수나 코드 커버리지보다 먼저 볼 것은 항상 어떤 결함에서 실패하는가다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.