3 분 소요

이 글은 테스트 검증에 대한 코드를 리뷰하면서 느꼈던 나의 생각을 정리한 글이다. 옳고 그름, 맞고 틀리고를 떠나 온전히 나의 생각을 정리하고 이런 고민을 했던 것을 기록하기 위함이니 혹시 다른 의견이 있다면 댓글로 적어주시면 좋은 토의를 해볼 수 있을 것 같다.

Background

이 이야기는 특정 도메인의 상태를 변경하는 API를 추가하는 기능을 추가한 PR을 리뷰하면서 시작한다. 이야기를 이어나가기 전에 이해를 돕기 위해 간단한 코드로 설명을 이어가고자 한다.

도메인 모델

class Foo {
    val a: String = ""
    val b: String = ""
    val c: Int = 0
}

API 명세

  1. 단일 Foo 조회
  2. Fooc 값 변경

1번 API는 이미 개발이 완료된 상태였고 작업자는 2번 API에 대한 개발을 진행하게 되면서 기존 테스트 케이스 아래에 새로운 케이스를 추가하게 된다.

Test Code

class ApiTest {

    fun "단일 조회 API 에상하는 값을 반환한다"() {
        // ID를 이용하여 단일 조회

        // 검증
        response shouldBe """
            {
                "a": "값A",
                "b": "값B",
                "c": 1
            }
        """
    }

    fun "값 변경 API `c`값을 변경한다"() {
        // 변경 API 호출

        // 단일 조회 API 호출

        // 검증
        response.c shouldBe 3
    }

}

여기서 마지막 검증에 대한 코드로 리뷰어와 작업자는 의견이 달라 해당 내용에 대해 토의를 하게 된다. 작업자는 2번 APIc값을 변경하는 기능이고 기존에 ab를 검증하는 코드가 존재하기 때문에 c의 변경에 대한 검증만 있으면 된다라는 의견이었다.

반면 리뷰어는 1번 API에서 ab도 함께 반환하고 있고, 해당 속성들이 값 변경 API에 의해 변경되지 않음을 함께 검증함으로써 검증력을 높이기 위해 아래와 같이 a, b, c 모두 검증하는 코드를 작성해야 한다는 의견이었다.


// ...생략

fun "값 변경 API `c`값을 변경한다"() {
    // 변경 API 호출

    // 단일 조회 API 호출

    // 검증
    response shouldBe """
        {
            "a": "값A",
            "b": "값B",
            "c": 3
        }
    """
}

Opinion

위 사례에 대해 나의 의견은 “c의 항목만 검증하면 된다” 이다. 그 이유는 아래와 같다.

필요이상의 검증을 하지 않는가

TDD에서 피해야할 관행에 대한 내용을 위키에서 볼 수 있는데 그 내용은 아래와 같다.

  • 이전에 테스트 케이스에 의해 테스트 케이스의 결과가 달라져서는 안된다.
  • 테스트 케이스가 서로 의존관계를 가져서는 안된다.
  • 정확한 실행 동작 타이밍 또는 성능을 테스트를 해서는 안된다.
  • All knowing oracle 즉 필요이상의 검증을 하지 않아야 한다.
  • 구현의 세부사항에 대해서 테스트하지 않아야 한다.
  • 테스트 코드의 실행이 느려지지 않도록 주의한다.

주어진 안티 패턴 중 위 사례에 해당할만한 패턴이 바로 "All knowing oracle" 즉 필요이상의 검증을 하지 않아야 한다. 이다.

조회시 ab의 속성은 첫번째 테스트 케이스에서 이미 검증을 완료하였다. 그러므로 두번째 테스트 케이스에서는 ab의 속성을 또 다시 검증하는 것은 필요이상의 검증을 한다고 생각한다.

필요이상의 검증은 또 시간이 지남에 따라 더 비싸지고 깨지기 쉬워진다. 1번 API에서 제공하는 응답값은 계속해서 변화해 갈 것인데 모든 테스트에 해당 속성값을 검증하는 코드를 넣는다고 상상해보자. 검증코드는 1번 API가 변경될 때마다 함께 변경되어야 할 것이고 해당 API의 변경만으로 모든 테스트 케이스가 실패하게 될 수도 있다.

복잡성이 증가하지 않는가

테스트 코드도 운영 코드와 같이 지속적으로 관리되고 리펙토링 되어야할 중요한 코드이다. 요구사항이 변경되면 가장 먼저 변경되어야 하므로 지속적으로 변경을 유발할 수 있기 때문에 중복을 제거하거나 가독성을 높이기 위한 노력을 끊임없이 이어가야 한다.

만약 리뷰어가 제시한 검증을 계속해서 사용하는 경우 검증해야할 속성이 많지 않을 때에는 모르지만 속성이 많은 API 응답인 경우에는 코드의 양이 무척 많아질 것이고 중복에 대한 부담감은 끊임없이 증가할 것이다. (중복을 제거하기 위해 별도의 함수로 처리해도 되지 않느냐 라고 말할 수 있겠지만 다른 속성값이 변경됨을 테스트 하는 경우에 해당 함수만으로는 일반화 하기가 쉽지 않다)

중복된 코드가 많아지고 테스트 케이스에서 실제로 검증하려는 것이 무엇인지 흐려질때 우리의 테스트 코드는 유지보수성이 점점 떨어지게 될 것이다.

검증 코드가 운영코드의 변경을 요하는가

리뷰어가 제시한 검증 코드를 아래와 같이 조금 바꿔보자


// ...생략

fun "값 변경 API `c`값을 변경한다"() {
    // 변경 API 호출

    // 단일 조회 API 호출

    // 검증
    response.a shouldBe "값A"
    response.b shouldBe "값B"
    response.c shouldBe 3
}

TDD로 개발한다면 마지막 response.c shouldBe 3 코드는 테스트의 실패를 먼저 경험할 수 있다. 왜냐하면 운영코드의 변경을 요구하기 때문이다. 하지만 response.a shouldBe "값A"response.b shouldBe "값B"는 테스트의 실패를 먼저 경험할 수 없을 것이다. 운영코드를 변경하지 않는데 다시 또 검증코드를 넣는다면 그 코드는 불필요한 코드가 될 것이다.

만약 TDD가 아니라 개발 후 테스트를 작성하는 TLD를 한다면 해당 의견에 대해서는 아마 느끼기 쉽지 않을 것이라 생각한다. 왜냐하면 라이프사이클에서 테스트의 실패를 먼저 경험해야하지 않기 때문이다. TDD를 통해 우리는 Red에서 Green으로 가기위해 변경해야할 코드에만 집중할 수 있다.

Wrap Up

이번 사례를 통해서 테스트 코드 작성시 검증에 대한 고민을 좀더 다양하게 할 수 있는 계기가 되어서 아주 좋은 경험이었다고 느꼈다. 테스트 코드를 작성하는 것은 언뜻 보기엔 쉬워 보일 수 있으나 어떻게 작성하고 어떻게 검증하며 어떻게 관리하느냐는 좋은 코드를 작성하는 것만큼이나 어려운 고민이고 정답이 없기에 끊임없이 고민하고 고쳐가야 한다고 생각한다. 앞으로 이런 고민을 할 수 있는 기회가 많아지길 바란다.