AI가 짠 테스트 믿어도 될까

JYC·2026년 9월 14일
post-thumbnail

요즘 AI에게 구현을 맡기면 테스트까지 같이 써준다.

Controller 구현을 맡기면 @WebMvcTest 기반 테스트도 만들어주고, 정상 케이스뿐 아니라 예외 케이스까지 꽤 그럴듯하게 작성해준다.

그리고 실행해 보면 전부 초록불이다.

다 짰다 와~~~ 하고 넘긴다.

그런데 여기서 문제가 하나 생긴다.

초록불은 테스트가 통과했다는 사실만 알려준다.
그 테스트가 내가 원하는 것을 실제로 검증하고 있는지는 알려주지 않는다.

직접 겪어보니 이 둘은 생각보다 꽤 다른 문제였다.

이 글은 AI가 작성한 테스트가 멀쩡하게 통과하면서도 요구사항을 놓쳤던 사례와, 그 테스트가 코드 리뷰에서도 그대로 살아남았던 이유에 대한 이야기다.


엥 테스트 통과했잖아요

응답에서 값이 없는 필드는 아예 제외해야 하는 API가 있다고 해보자.

예를 들어 nickname이 없다면 다음처럼 나가야 한다.

{
  "id": 1
}

그래서 테스트도 자연스럽게 이렇게 작성했다.

@Test
void nickname이_없으면_응답에서_빠진다() throws Exception {
    mockMvc.perform(get("/users/1"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.nickname").doesNotExist());
}

이름만 보면 꽤 정확해 보인다.

doesNotExist().

nickname이 존재하지 않아야 한다는 뜻처럼 읽힌다.

그런데 실제 응답이 아래처럼 나가도 테스트가 통과할 수 있다.

{
  "id": 1,
  "nickname": null
}

Spring의 doesNotExist()가 확인하는 것은 단순히 JSON Path 자체가 없는지가 아니다.

해당 경로에 non-null 값이 존재하지 않는지를 확인한다.

그래서

"nickname": null

과

// nickname 자체가 없음

을 내가 기대했던 방식으로 구분해주지 않는다.

이걸 확인한 방법은 단순했다.

DTO에 붙어 있던 다음 설정을 잠깐 제거했다.

@JsonInclude(JsonInclude.Include.NON_NULL)

그러면 응답에 null인 필드가 그대로 포함된다.

{
  "id": 1,
  "nickname": null
}

여기서 테스트가 실패할 거라고 생각했다.

그런데 여전히 초록불이었다.

내가 막으려고 했던 회귀는 이미 발생했는데, 테스트는 아무 문제도 없다는 듯 통과하고 있었다.

정말로 JSON Path 자체가 없는 것을 확인하고 싶다면 Spring에는 별도의 API가 있다.

.andExpect(jsonPath("$.nickname").doesNotHaveJsonPath());

이름은 비슷하지만 의미는 다르다.
doesNotExist()는 non-null 값의 부재를 보고, doesNotHaveJsonPath()는 null을 포함해 경로 자체가 없는지를 본다.

둘의 이름이 비슷하다 보니 코드를 대충 읽었을 때는 문제를 눈치채기 쉽지 않았다.

무엇보다 테스트가 이미 통과하고 있었기 때문에 굳이 의심하지 않았다.


테스트를 한번 망가뜨려봤다

이 문제를 발견한 뒤부터 중요한 테스트는 가끔 반대로 확인해본다.

테스트가 지키고 있다고 생각하는 동작을 일부러 깨뜨려보는 것이다.

@JsonInclude 제거
        ↓
테스트 실행
        ↓
그런데 GREEN

이렇게 해보면 테스트가 실제로 무엇을 보고 있는지 꽤 빨리 드러난다.

예를 들어 이런 식이다.

@JsonInclude를 제거한다.

조건문을 반대로 바꾼다.

예외를 던지는 코드를 잠깐 지운다.

반환 값을 일부러 틀리게 만든다.

이 상태에서도 테스트가 계속 통과한다면 뭔가 이상하다.

적어도 내가 그 테스트로 막고 싶었던 문제는 제대로 잡고 있지 않다는 뜻이다.

이런 접근을 체계적으로 자동화한 것이 mutation testing이고, Java에서는 PIT 같은 도구가 대표적이다.

그렇다고 처음부터 꼭 mutation testing 도구까지 붙일 필요는 없다고 생각한다.

중요한 테스트 몇 개만 직접 망가뜨려봐도 잘못된 assertion을 찾는 데 꽤 도움이 된다.

특히 AI가 구현과 테스트를 한 번에 만들어주는 환경에서는 이 과정이 더 중요해졌다.

직접 테스트를 작성하면 보통 중간에 한 번쯤은 빨간불을 보게 된다.

테스트 작성
   ↓
  RED
   ↓
  구현
   ↓
 GREEN

이 과정에서는 자연스럽게 생각하게 된다.

왜 실패하지?

지금 내가 뭘 검증하고 있지?

그런데 AI에게 구현과 테스트를 한 번에 맡기면 처음 보는 결과가 이미 GREEN인 경우가 많다.

구현도 있고, 테스트도 있고, 실행도 성공했다.

그러면 그냥 잘 됐다고 생각하고 넘어가기 쉽다.

나도 그랬다.


그런데 리뷰 도구도 테스트는 건너뛰고 있었다

처음에는 그래도 크게 걱정하지 않았다.

AI가 만든 코드를 그대로 믿지 않으려고 별도의 코드 리뷰 도구도 붙여두고 있었기 때문이다.

내가 사용한 것은 알리바바에서 공개한 open-code-review였다.

변경된 git diff를 기준으로 코드의 문제를 찾아주는 도구다.

그래서 나는 대충 이런 구조라고 생각하고 있었다.

AI가 구현
   ↓
AI가 테스트
   ↓
테스트 실행
   ↓
코드 리뷰

구현에서 놓친 문제는 테스트가 잡고,

테스트가 놓친 문제는 리뷰에서 한 번 더 잡아줄 거라고 생각했다.

그런데 한참 사용한 뒤 기본 설정을 보다가 하나를 발견했다.

테스트 파일이 기본 리뷰 대상에서 제외되어 있었다.

결국 구현 코드는 리뷰를 받고 있었지만, 테스트 코드는 실행만 되고 리뷰는 받지 않고 있었다.

이걸 보고 앞에서 겪었던 doesNotExist() 문제가 다시 떠올랐다.

구현을 AI가 작성했다.

테스트도 같은 AI가 작성했다.

그 테스트의 assertion은 요구사항을 제대로 확인하지 못하고 있었다.

그런데 리뷰 도구는 테스트 파일 자체를 보지 않았다.

결국 테스트는 구현을 검증하고 있었지만, 그 테스트가 제대로 작성됐는지는 아무도 확인하지 않고 있었다.

문제는 구현이랑 테스트를 같은 AI가 만들었다는 점이었다.

AI가 요구사항을 한 번 잘못 이해하면 구현과 테스트가 각각 따로 틀리는 게 아니라, 둘 다 같은 방향으로 틀릴 수도 있다.

예를 들어 어떤 요구사항을 A라고 이해해야 하는데 AI가 B라고 이해했다고 해보자.

그러면 구현도 B로 만들고,

테스트도 B를 기준으로 작성한다.

그 상태에서는 테스트가 아무리 잘 통과해도 이상할 게 없다.

둘이 같은 착각을 하고 있으니까.


마지막 리뷰는 다른 에이전트에게 맡겼다

이후에는 최종 리뷰 방식을 조금 바꿨다.

구현하던 에이전트에게 그대로

지금 작성한 코드 리뷰해줘.

라고 하는 대신, 별도의 에이전트나 별도 세션에서 최종 변경사항을 다시 보게 했다.

구현 과정에서 오간 대화도 최대한 넘기지 않았다.

대신 리뷰하는 데 필요한 것만 전달했다.

- 변경된 diff
- 처음 요구사항
- 필요한 경우 구현 계획

리뷰할 때 확인해달라고 하는 항목도 조금 더 구체적으로 적었다.

1. 변경사항이 요구사항과 다른 부분이 있는지 확인한다.

2. 테스트가 통과하더라도 실제 장애가 발생할 수 있는 경로를 찾는다.

3. 각 테스트의 assertion이 테스트 이름과 요구사항을 실제로 검증하고 있는지 확인한다.

4. 정상 케이스뿐 아니라 실패 경로가 빠진 곳을 확인한다.

5. 스타일보다 실제 버그 가능성을 우선해서 지적한다.

6. 지적할 때는 근거가 되는 file:line을 함께 제시한다.

여기서 특히 중요하게 보는 건 3번이다.

테스트 파일이 존재하는지만 보는 게 아니라,

이 assertion이 정말 내가 원하는 동작을 확인하고 있는지를 보게 한다.

물론 리뷰 에이전트를 따로 둔다고 문제가 다 해결되는 것은 아니다.

리뷰하는 AI도 충분히 틀릴 수 있다.

그래서 리뷰 결과를 그대로 받아들이는 게 아니라, 지적한 내용을 다시 코드와 테스트에서 확인한다.

그래도 구현하던 맥락을 그대로 들고 자기 코드를 다시 보는 것보다는, 처음 보는 입장에서 한 번 더 확인하게 하는 편이 나았다.


성공을 한 번 의심해보기

예전에는 테스트 결과가 이렇게 나오면 꽤 안심했다.

BUILD SUCCESSFUL

Tests: 42 passed

지금도 테스트가 통과하는 건 당연히 중요하다.

다만 AI가 구현과 테스트를 같이 작성했다면 초록불 하나만 보고 끝내지는 않는다.

특히 중요한 요구사항이라면 한 번쯤 반대로 확인한다.

잘못된 동작을 넣었을 때 이 테스트가 정말 실패하는가?

그리고 리뷰할 때도 구현 코드만 보는 게 아니라 테스트 코드를 같이 본다.

지금 사용하는 흐름을 정리하면 대략 이렇다.

AI 구현
  ↓
AI 테스트 작성
  ↓
테스트 실행
  ↓
중요한 동작을 일부러 깨뜨려 확인
  ↓
별도 맥락에서 구현 + 테스트 diff 리뷰
  ↓
최종 판단

구현도 AI가 만들고 테스트도 AI가 만드는 환경에서는 테스트가 있다는 사실만으로는 충분하지 않았다.

구현과 마찬가지로 테스트도 검증 대상이었다.

profile
열심히 하기 1일차

0개의 댓글