본문으로 건너뛰기
QA-Lab
중급25

항상 통과하는 가짜 테스트 찾아내기

초록색 통과 표시가 떠도, 그 테스트가 실제로 아무것도 검증하지 않을 수 있다. AI가 만든 테스트에서 반복적으로 나타나는 가짜 테스트 패턴을 익힌다.

  • #가짜테스트
  • #AAA패턴
  • #AI코드검증

개념

통과 표시가 곧 검증을 의미하지 않는다

모듈 10(레슨 1)에서 배운 AAA(Arrange-Act-Assert) 패턴을 떠올려보자 — 진짜 테스트는 Assert 단계에서 의미 있는 검증을 해야 한다. AI가 생성한 테스트 코드에는, 형식적으로는 테스트처럼 보이지만 실제로는 아무것도 검증하지 않는 패턴이 반복적으로 나타난다.

패턴 1 — 어서션이 아예 없다

test("주문 생성이 동작한다", () => {
  createOrder({ productId: "P001", quantity: 1 })
  // 끝. expect()가 없다.
})

이 테스트는 함수를 호출만 할 뿐, 결과가 무엇인지 전혀 확인하지 않는다. 함수 내부에서 에러가 나지 않는 한 항상 "통과"로 표시된다 — 모듈 10(레슨 5)에서 배운 "라인 커버리지는 올라가지만 실질적으로 검증하지 않는 테스트"의 전형이다.

패턴 2 — 항상 참인 어서션(Tautological Assertion)

test("주문 금액이 계산된다", () => {
  const total = calculateTotal(cart)
  expect(total).toBeDefined()  // total이 undefined만 아니면 무조건 통과
})

toBeDefined()는 값이 존재하기만 하면 통과한다 — 그 값이 0이든, -500이든, NaN이든 전혀 상관하지 않는다. 실제로 검증하고 싶었던 것("금액이 정확히 계산됐는가")과는 아무 관련이 없는 느슨한 확인이다.

패턴 3 — 검증 대상 자체를 확인한다(순환 논리)

test("할인이 적용된다", () => {
  const discountedPrice = applyDiscount(1000, 0.1)
  expect(discountedPrice).toBe(applyDiscount(1000, 0.1))  // 같은 함수를 두 번 호출해 비교
})

같은 함수를 두 번 호출해서 서로 비교하면, 그 함수가 일관되게 같은 결과를 낸다는 것만 확인할 뿐, 그 결과가 실제로 맞는 값인지는 전혀 검증하지 않는다. 항상 통과할 수밖에 없는 구조다.

패턴 4 — 목(Mock)이 실제 로직을 대체해버린다

모듈 10(레슨 3)에서 배운 테스트 더블을 떠올려보자. AI가 테스트를 쉽게 통과시키려고, 검증하려던 로직 자체를 목으로 대체해버리는 경우가 있다.

test("할인 계산 로직이 정확하다", () => {
  jest.mock("./discountCalculator", () => ({
    calculateDiscount: () => 900  // 실제 로직 대신 고정값을 반환하게 함
  }))
  expect(calculateDiscount(1000, 0.1)).toBe(900)  // 당연히 통과한다
})

실제 계산 로직은 전혀 실행되지 않고, 미리 정해둔 값이 그대로 나오는지만 확인한 것이다 — 이건 검증하려던 대상을 검증하지 않은 것과 같다.

가짜 테스트를 찾아내는 체크리스트

1. Assert(expect) 문이 있는가?
2. 어서션이 구체적인 값을 확인하는가, 아니면 "존재하는지"만 느슨하게 보는가?
3. 검증 대상과 비교 대상이 서로 독립적인가(순환 논리가 아닌가)?
4. 목이 검증하려던 핵심 로직 자체를 대체하고 있지 않은가?

이 체크리스트는 모듈 10(레슨 1)에서 배운 AAA 패턴을 거꾸로 검증하는 도구로 쓰는 것이다 — Act는 있는데 Assert가 의미 없다면, 그건 테스트가 아니라 코드를 실행만 해본 것이다.

일부러 실패시켜보기(Mutation의 발상)

가장 확실한 검증법은, 테스트 대상 코드를 일부러 틀리게 바꿔보고 테스트가 실패하는지 확인하는 것이다.

1. calculateTotal 함수의 로직을 일부러 틀리게 수정한다
   (예: 할인율을 반대로 적용하게 만듦)
2. 관련 테스트를 실행한다
3. 테스트가 실패하면: 진짜 검증하고 있었다는 뜻
4. 테스트가 여전히 통과하면: 가짜 테스트였다는 뜻 — 그 테스트는
   코드가 틀려도 잡아내지 못한다

이 기법은 실제로 "뮤테이션 테스팅(Mutation Testing)"이라는 이름으로 자동화 도구까지 있는 개념이다 — 코드를 의도적으로 망가뜨려서, 테스트 스위트가 정말 그 망가짐을 잡아내는지 검증한다.

실무에서 왜 필요한가

"AI가 테스트 커버리지 90%를 달성했다"는 보고가, 실제로는 절반이 어서션 없는 가짜 테스트였다면 그 숫자는 완전히 허상이다. 이 레슨의 체크리스트와 "일부러 틀리게 바꿔보기" 기법을 아는 QA는, 겉으로 그럴듯한 테스트 스위트 뒤에 숨은 빈 껍데기를 찾아낼 수 있다.

실습 과제

과제 1 — 가짜 테스트 판정하기 (15분)

다음 세 테스트가 이 레슨에서 배운 어떤 패턴(어서션 없음/느슨한 어서션/순환 논리/과도한 목)에 해당하는지 판정한다.

// A
test("사용자 생성", () => {
  const user = createUser({ name: "홍길동" })
})
 
// B
test("가격 표시", () => {
  const price = formatPrice(15000)
  expect(price).toBeTruthy()
})
 
// C
test("환율 변환", () => {
  jest.mock("./exchangeRate", () => ({ getRate: () => 1300 }))
  expect(convertCurrency(100, "USD")).toBe(130000)
})

과제 2 — 일부러 틀리게 만들어보기 (10분)

"주문 취소 시 재고를 다시 늘린다"는 함수와, 그걸 검증하는 테스트가 있다고 하자. 이 함수를 일부러 틀리게(재고를 늘리지 않게) 바꿨을 때 테스트가 실패해야 진짜 테스트다. 이 검증을 어떻게 실행할지 순서대로 적는다.

자가 체크리스트

  • 어서션이 없는 테스트가 왜 항상 "통과"로 나오는지 설명할 수 있다
  • 느슨한 어서션(toBeDefined 등)과 구체적인 어서션의 차이를 설명할 수 있다
  • 순환 논리 어서션이 왜 실질적인 검증이 아닌지 설명할 수 있다
  • 목이 핵심 로직을 대체해버리는 문제를 설명할 수 있다
  • "일부러 틀리게 바꿔보기"로 테스트의 진짜 여부를 확인하는 방법을 설명할 수 있다

흔한 실수

  • 통과하는 테스트 개수만 세고 내용은 확인하지 않는다. 개수는 많아도 실질적 검증은 거의 없을 수 있다.
  • toBeDefined(), toBeTruthy() 같은 느슨한 어서션을 구체적인 값 확인과 같은 수준으로 취급한다. 훨씬 약한 검증이다.
  • 목을 쓴 테스트는 무조건 의심 없이 신뢰한다. 목이 검증 대상 로직 자체를 대체해버렸는지 항상 확인해야 한다.

참고 자료

  • Kent Beck, Test-Driven Development: By Example — 의미 있는 어서션 작성 원칙(모듈 10 레슨 1과 동일한 참고 자료)
  • Stryker Mutator 공식 문서 — 뮤테이션 테스팅 도구의 공식 소개