Post

(Test) TDD, 단위 테스트, 통합 테스트

(Test) TDD, 단위 테스트, 통합 테스트

📝 작성 배경

최근 우연찮은 기회로 평소에 내가 생각한던 테스트 와 TDD 에 대한 생각을 점검 받고, “좀더 좋은 테스트란 어떤 테스트인가?” 에 대해 고민할 수 있는 기회가 생겨 해당 과정을 통해 얻은 부분과 내 생각과는 조금 다른 부분에 대해 정리해보려고 한다.

해당 글은 독자가 TDD 가 어떤 개발 방법론인지, 그리고 테스트 코드를 작성해본 경험이 조금은 있다는걸 전재하에 작성된 글 입니다.


👓 선 3줄 요약

  1. TDD 는 순서보다 ‘왜’ 테스트를 작성하는지가 더 중요하다.
  2. 단위 테스트는 책임의 범위를 명확히 하고 중복을 피해야 한다.
  3. 통합 테스트는 ‘통합적으로만 검증 가능한 것’에 집중해야 한다

🔄 TDD

TDD 는 테스트 주도 개발을 뜻하는 용어로, 말 그대로 개발을 진행할때 테스트가 개발을 주도한다는 의미이다.
한창 핫 했던 용어 답게 검색을 해보면 정말 많은 TDD 관련 주제의 글들을 매우 손 쉽게 찾아 볼 수 있다.

개발을 하기 전, 테스트를 먼저 작성하여 READ-GREEN-REFACTOR 순서대로 개발을 진행하는 방법론이다.

  • READ: 실패 하는 테스트 작성
  • GREEN: 테스트가 통과하게끔 어떤 방식으로든 로직 구현
  • REFACTOR(Refactor): 테스트가 통과하는 선에서 코드 수정 단, 동작은 변경되지 않는다.

위 내용이 TDD 에 핵심이라 할 수 있는데, 사실 이 핵심 때문에 TDD 를 많은 사람들이 어려워 한다고 생각한다.
실패하는 테스트를 작성하고, 해당 테스트가 통과하게끔 로직을 작성하고, 리펙터링하고 이걸 반복적으로 수행하면서
기능을 개발해 나아가는건데 사실 말은 쉽지만, 저걸 반복한다는건 매우 어렵다. 1사이클은 매우 쉬울 수 있다.
하지만 그 이후 이걸 반복할때 테스트는 어떻게 작성해야 하며, 기능 개발은 어디까지 진행을 해야할까?
이 부분이 참 어려운것 같다.

예를 들어 유저의 포인트를 조회한다고 해보자.
해당 기능을 TDD 를 사용해서 개발한다고 할때 첫번째로 해야하는건 실패하는 테스트를 작성하는것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class PointService(
    // ...
) {
    fun retrieveUserPoint(id: Long): Unit? {
        //...
        return null
    }
}

class PointServiceTest {
    // ...

    @Test
    fun retrieveUserPointTest() {
        val userId = 1L
        val actual = pointService.retrieveUserPoint(userId)
        assertNotNull(actual)
    }
}

위 와 같이 간단하게 실패하는 테스트를 작성해 볼 수 있다.
우선 null 을 반환하고 테스트 코드에서는 null 이 아님을 검증하면 된다.
Read 가 되었으니 이제 다음 단계인 Green 을 해보자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class PointService(
    // ...
) {
    fun retrieveUserPoint(id: Long): Long {
        //...
        return 100L
    }
}

class PointServiceTest {
    // ...

    @Test
    fun retrieveUserPointTest() {
        val userId = 1L
        val actual = pointService.retrieveUserPoint(userId)
        assertNotNull(actual)
    }
}

return 값을 null 이 아닌 100L 으로 변경했다. 이제 테스트가 통과하게 된다.
그럼 다음엔 무엇을 해야 할까? 바로 REFACTOR(Refactor) 를 하면 된다.
근데 코드를 살펴보니 딱히 뭘 수정 해야 할지 모르겠다. 그럼 다음으로 넘어가보자.

사실 여기까지만 봐도 조금은 느낄 수 있을 것 같다. 이걸 굳이 이렇게 까지 해야하는걸까?
해당 방식은 처음 TDD 를 배우고 테스트 코드를 작성해보는 분들이 학습 할때는 권장되는 방식이지만,
어느 정도 테스트 작성에 익숙한 분들에게는 그다지 추천되지 않는 방식이다.

전통적인 TDD 는 저게 맞을지 몰라도, 내가 생각하는 TDD 는 저런 방식이나 순서가 중요한게 아니라고 생각한다.
우리가 테스트를 작성하는 이유는 여러 가지가 있을 수 있는데 내가 생각하는 TDD 에서의 테스트를 작성하는 이유는

  1. 좋은 설계를 위해서
  2. 핵심 기능의 안정성을 높이기 위해 이 두가지라 생각된다.

테스트를 작성하다보면 내가 작성하려했던 로직이 뭔가 이상한것 같다는 생각을 할때가 종종 있다.
로직이 이상하다는건 내가 머리속으로 설계한 부분이 이상한걸 수도 있고, 내가 놓친 부분이 있을 수도 있다는걸 뜻한다.

테스트 작성 없이 순전히 기능만 쭉쭉 작성하다 나중에 이상함을 느껴 확인 했을 때는
수정해야 하는 범위가 너무 넓어져 작업 시간이많이 늘어났던 경험을 종종 했었다.

근데 TDD 를 배우고 적용시킨 후에는 뭔가 이상한 부분이나, 놓친 부분을 테스트를 작성하다 파악하게 되고,
그때마다 수정하다 보니 기능 구현이 매끄럽게 진행된다고 느꼈던 적이 많다.

TDD 를 할때는 방식이나, 순서 보다는 우리가 왜 테스트를 작성해야 하는지 테스트 작성의 이유에 좀더 집중하면 좋을 것 같다.

순서나 작성 방식은 처음에는 아예 모르니 전통적인 TDD 의 방식을 따라하다, 자신만의 작성 방법이 생겼을때 혹은
어디선가 좋은 방식을 보게 되었을때 그때 그 방식을 적용시켜보면 좋을 것 같다.


▶️ 단위 테스트

단위 테스트, 사실 테스트를 크기에 따라 분리하는건 논쟁이 많은 부분이다. 단위 테스트의 범위는 어디까지인가?
이 글에서는 단위 테스트는 그냥 하나의 메서드(함수) 에 대한 테스트 라고 생각하면 좋을 것 같다.

주로 나는 ServiceController 에 대한 단위 테스트를 작성하는데
가끔 Repository 에 대한 테스트도 작성하곤 한다.
각각의 레이어에는 핵심적인 역할들이 있으며 나는 그 역할에 대해 테스트를 진행했다.

  1. service: 비즈니스 로직 검증
  2. controller: 사용자의 입력 검증, 반환 데이터 검증
  3. repository
    • ORM 사용 시: 내가 의도한 쿼리가 발생하고 적용되는지
    • Spring Data JPA: 해당 라이브러리를 통한 쿼리 호출에 대해서는 테스트 작성을 하지 않았다.

위 와 같은 기준을 갖고 테스트를 작성했다.
다만 여기서 중요한건 레이어별로 이미 테스트를 진행한건 테스트를 중복해서 작성하지 않는다는 부분이다.

예를들어 이미 Controller 에서 사용자의 입력 값을 검증 했다면, Service 에서 다시 사용자의
입력값을 검증하는 테스트는 작성하지 않는것이다.

테스트를 레이어별로 역할에 따라 작성했는데 중복적인 테스트를 작성하는건 생산성 측면도 그렇고
테스트를 작성하는 두 가지 이유에 모두 적합하지 않다고 생각했기 때문이다.

사용자 입력을 서비스에서 테스트 해보고 싶어, 서비스에 사용자 입력을 검증하는 로직을 넣는다면 과연 그게 좋은 설계일까?


▶️ 통합 테스트

이 부분에서 많은 생각이 달라졌다.
나는 기존에 통합 테스트와 단위 테스트는 역할 자체가 다른거라 생각했다. 단위 테스트를 아무리 잘 작성해도
결국 로직을 합쳤을때 문제가 발생할 수 있기 때문에 그걸 방지하기 위해 작성하는 테스트가 통합 테스트 라고 생각했었다.

단위 테스트는 말 그대로 기능 단위의 테스트고, 통합 테스트는 말 그대로 통합적인 테스트라 생각했는데,
그게 아닐 수도 있다는걸 느꼈고 다시 한번 이부분에 대해 생각하게 되었다.

통합 테스트에서 모든 로직을 재 검증하는건 사실 테스트를 중복적으로 작성하는것과 마찬가지다.
“통합 테스트는 통합 테스트로만 검증할 수 있는 부분을 테스트 하는 것 이지, 모든 기능에 대해 테스트 하는건 아니다.” 라는
이야기를 듣고 적지 않은 충격을 먹었다. 물론 이건 모든 사람이 같은 생각을 하는건 아니라고 생각된다. 하지만 통합 테스트를 작성하면서 “굳이 이걸 또 검증할 필요가 있을까?”
라고 종종 생각은 했지만, 그래도 해야지 하면서 작성했던 나한테는 큰 인사이트가 되었다.

통합 테스트로 검증해야 하는 부분과 단위 테스트로 검증해야 하는 부분을 아직까지는 잘 구분하지 못하겠지만,
이건 이런 생각을 갖고 통합 테스트를 작성해본적이 없어 그런것 같다.

막연하게 모든 기능에 대해 테스트를 작성했던 나를 뒤돌아 보면서 정말 많은 생각을 하게 되었던 순간이었다.

앞으로 통합 테스트로만 검증할 수 있는, 통합 테스트를 작성해야만 하는 등의 생각을 좀 하면서 통합 테스트를 작성해 볼 예정이다.
작성하다가 “통합 테스트는 모든 기능에 대해 테스트하는게 맞아!” 라고 생각이 다시 바뀔수도 있다.
다만, 그런 고정된 생각을 갖고 있던 내가 다른 시선으로 통합 테스트를 볼 수 있게 된것 만으로도 많은걸 얻었다고 할 수 있을 것 같다.


💡 마무리

사실 이번 글이 다른 분들께 도움이 될거라 생각되진 않는다. 다만 어딘가에 테스트에 대한 내 생각을 정리하면
좋을 것 같다 생각했었는데 이번 기회에 정리해 볼 수 있어 좋았던것 같다.

This post is licensed under CC BY 4.0 by the author.