Unit Testing 2장 : 단위 테스트란 무엇인가

    들어가기 전

    이 글은 단위 테스트 2장, 단위 테스트란 무엇인가를 공부하며 작성한 글입니다. 


    2.1 '단위 테스트'의 정의

    단위 테스트는 다음 세 가지 조건을 모두 만족시키는 테스트다.

    • 작은 코드 조각(단위)를 검증한다.
    • 빠르게 수행되어야 한다.
    • 격리된 방식으로 처리되어야 한다. 

    여기서 얼마나 작은 조각인지와 어떻게 격리되어야 하는지는 사람마다 서로 다른 관점이 있다. 아래 장에서 그 부분을 살펴보고자 한다. 


    2.1.1 격리 문제에 대한 런던파의 접근

    런던파는 격리를 통해 하나의 '클래스'에만 집중하는 단위 테스트를 작성한다. 이를 달성하기 위해 값 객체를 제외한 모든 의존성을 모두 테스트 대역(Mock)으로 바꿔치기해서 테스트 한다. 이렇게 테스트 했을 때의 장점은 다음과 같다.

    • 테스트가 실패하면 코드베이스의 어느 부분이 고장났는지 알기 쉽다. 
    • 복잡한 객체 그래프를 직접 구현할 필요없이, 클래스에만 집중할 수 있다. 

    첫번째는 하나의 클래스 내부에서만 테스트 하고 나머지는 모두 테스트 대역을 사용하기 때문에 다른 클래스가 오작동 하는 것에 전혀 영향을 받지 않는다는 것이다. 두번째는 클래스가 가지는 모든 의존성을 가짜 객체(테스트 대역)으로 바꾸기 때문에 직접 모든 객체 그래프를 작성하지 않아도 된다는 것이다. 이런 장점이 있기 때문에 런던파의 단위 테스트는 좋아 보일 수 있다. 하지만 장점이 있는만큼 단점 역시 존재한다.

    그림으로 살펴보면 위와 같다. 위의 그림은 객체 그래프의 대체를 의미한다. 런던파의 단위 테스트는 테스트 대상 클래스가 가지는 모든 의존성으로 테스트 대역(Mock)으로 교체한다. 그래서 각 dependency가 가지는 또 다른 dependency(객체 그래프)를 구현하지 않아도 된다. 

    따라서 런던파의 테스트는 항상 위에서 볼 수 있듯이, 테스트 클래스 하나는 제품코드 하나에 대응되는 형태가 된다. 런던파는 단위 테스트의 단위를 '클래스'로 본다. 


    2.1.2 격리 문제에 대한 고전파의 접근

    런던파와 달리 고전파는 격리의 기준을 '테스트'로 가져간다. 고전파는 테스트끼리 격리되어야 한다고 이야기한다. 이것은 각 테스트의 결과가 다른 테스트에 영향을 주지 않는 것을 의미한다. 이것은 테스트를 어떠한 순서로, 그리고 여러번 실행해도 항상 동일한 결과가 나오는 것을 보장한다. 

    그렇다면 '테스트끼리의 격리'에 영향을 주는 요소는 무엇일까? 바로 테스트 간에 공유하는 의존성이다. 아래에서 의존성의 종류를 나눠보고자 한다

    • 공유 의존성 
      • 다음 두 가지를 모두 만족하는 의존성
        1. 테스트 간에 공유된다.
        2. 각 테스트에 서로 영향을 미칠 수 있다. 
      • 테스트 간에 공유되지만, 테스트에 영향을 미치지 않는 API 조회 서버는 '공유 의존성'이 아니다. 
      • 예시 : DB
    • 비공유 의존성
      • 테스트 간에 공유되지 않는다.
      • 내부에서 사용되는 클래스. 

    런던파는 하나의 클래스만 테스트 하기 위해 모든 의존성을 테스트 대역으로 교체했다. 하지만 고전파는 테스트끼리 격리하기 위해서 '공유 의존성'에 한해서만 테스트 대역을 사용할 수 있도록 한다. 

    공유 의존성은 왜 대체되어야 할까? 그 이유는 다음과 같다. 

    • 공유 의존성은 단위 테스트끼리 공유된다. 그런데 공유 의존성을 모든 단위 테스트마다 새롭게 생성해서 제공할 수 없다. DB나 파일을 매번 테스트 할 때마다 생성하면 번거롭기 때문이다. 따라서 테스트 대역으로 바꿀 이유가 된다. 
    • 공유 의존성을 테스트 대역으로 대체하면 테스트 속도가 빨라진다. DB, 파일은 네트워크 / 디스크 영역이다. 따라서 요청 / 응답에 걸리는 시간이 더 많이 걸리기 때문에 테스트 대역으로 바꿀 이유가 된다. 

    고전파의 테스트 격리의 관점에서 바라본다면 비공유 의존성, 공유 의존성은 다음과 같이 각각 대체 되어야 한다. 

    • 비공유 의존성은 유지한다. 
    • 공유 의존성은 테스트 대역으로 대체한다. 

    2.2 단위 테스트의 런던파와 고전파

    런던파와 고전파는 단위 테스트를 바라보는 시각이 서로 다르다. 그것을 표로 정리하면 다음과 같다.

      격리 주체 단위의 크기 테스트 대역 사용 대상
    런던파 단위 클래스 값 객체 제외 모든 의존성
    고전파 단위 테스트 동작 공유 의존성

     

    2.2.1 고전파와 런던파가 의존성을 다루는 방법

    런던파는 값 객체를 제외한 모든 의존성을 테스트 대역으로 바꾼다. 값은 그 자체로 상수값이기 때문에 바꿀 필요가 없다. 반면 고전파는 공유 의존성만 테스트 대역으로 바꾼다. 그림으로 살펴보면 아래와 같다. 

    모든 프로세스 외부 의존성은 공유 의존성을 의미하지는 않는다. 아래에서 볼 수 있듯이 API 조회 서버는 프로세스 외부 의존성이지만, 단순 조회는 테스트 간 영향을 미치지 않기 때문에 공유 의존성이 아닌 '프로세스 외부 의존성'으로 볼 수 있다. 아래 그림에는 공유 의존성 / 프로세스 외부 의존성을 분류한다

    • 싱글톤 : 내부 클래스다. 따라서 각 테스트마다 하나씩 만들어서 제공하면 된다. 쉽게 만들 수 있으므로 테스트 간에 공유 할 필요가 없다. 
    • 프로세스 외부 의존성 : API 조회 서버는 각 테스트의 결과에 영향을 미치지 않는다. 이 경우는 테스트 대역으로 대체하지 않고 그대로 사용해도 된다. 하지만 테스트에 많은 시간이 소모된다면 대체하는 것이 좋다.
    • DB :  이런 공유 의존성은 테스트 대역으로 대체해야한다. 왜냐하면 테스트마다 영향을 미치기 때문이다. 

    500


    2.3. 고전파와 런던파의 비교

    저자는 고전파의 테스트를 선호한다. 이 장에서는 고전파 / 런던파의 장단점을 알아보고자 한다. 런던파의 접근 방식은 다음 장점이 있다. 

    • 입자성이 좋다. 테스트가 세밀해서 한 번에 한 클래스만 확인한다
    • 연결된 객체 그래프가 커져도 테스트 하기 쉽다. 
    • 테스트가 실패만 어떤 기능이 실패했는지 확실히 알 수 있다. 

    위는 모든 의존성을 테스트 대역으로 대체했기 때문에 가져오는 이점이다. 테스트 대역은 아무런 작업도 하지 않기 때문에 테스트 대역의 오동작은 고려의 대상이 아니다. 현재 테스트하고 있는 테스트 클래스에서만 문제가 발생했다고 볼 수 있기 때문에 빠르게 오류를 잡을 수 있는 장점이 있다. 


    2.3.1 한번에 한 클래스만 테스트하기

    런던파는 클래스 단위로 테스트를 한다. 반면 고전파는 동작 단위로 테스트를 한다. 각 테스트의 느낌은 다음과 같다. 아래에서 볼 수 있듯이 세부적인 구현 사항을 하나씩 검증하는 것은 이상해보이고, 상대적으로 어떤 동작이 일어났는지를 검증하는 것이 이상적으로 보인다. 

    • 클래스 단위 : 우리집 강아지를 부르면 먼저 왼쪽 앞다리를 움직이고, 이어서 오른쪽 앞다리를 움직이고, 머리를 돌리고, 꼬리를 흔들기 시작한다. 
    • 동작 단위 : 우리집 강아지를 부르면, 바로 나에게 온다. 

    2.3.2 상호 연결된 클래스의 큰 그래프를 단위 테스트하기

    런던파는 모든 의존성을 테스트 대역으로 대체하기 때문에 객체 그래프가 간단해진다. (사실상 없다.) 반면 고전파는 공유 의존성을 제외한 모든 의존성을 직접 생성해야 하기 때문에 객체 그래프가 커질수록 어렵다. 이런 관점에서는 런던파가 좋을 수 있다. 

    하지만 저자는 다음과 같이 이야기한다. 객체 그래프가 커지는 것 자체가 문제이니 코드를 다시 한번 설계하고, 테스트 하기 쉽도록 바꾸라는 것이다. 

    객체 그래프가 아주 커진 것은 코드 설계에 문제가 있는 것이다. Mock으로 이것을 대체하는 것은 문제를 감추기만 할 뿐, 원인을 해결하지는 못한다. 

    2.3.3 버그 위치 정확히 찾아내기

    런던 스타일은 모든 의존성이 테스트 대역으로 대체되기 때문에 오류가 발생한다면, 테스트 대상 클래스의 문제에 거의 국한된다. 거의라는 말을 쓴 것은 구현 세부사항과 결합된 테스트들은 '거짓양성'을 나타내기 때문이다.

    반면 고전 스타일은 다른 의존성의 오동작이 테스트 대상 클래스에 영향을 미칠 수도 있다. 다른 클래스의 오동작이 전체 테스트에 큰 여파를 미칠 수 있다는 것이다. 이 부분은 충분히 우려할만하다. 그렇지만 쉽게 해결할 수 있다고 한다. 영향을 미친다고 하더라도 단계적으로 테스트 코드가 실패하며, 가장 최근에 수정한 부분이 문제일 겅시기 때문이다. 

    또한 단계적으로 테스트를 유발한다면, 수정된 그 코드가 '큰 역할'을 하는 코드이기 때문에 더욱 더 많은 테스트가 필요하는 것을 암시하고, 이것을 발견해 낼 수도 있게 된다. 


    2.3.4 고전파와 런던파 사이의 다른 차이점

    고전파와 런던파 사이에는 아래 두 가지 관점에서도 차이점이 존재한다.

    • TDD를 통한 설계 방식
      • 런던파는 모든 의존성이 테스트 대역으로 대체된다. 따라서 주로 하향식 TDD가 된다. 
      • 고전파는 모든 의존성을 직접 만들어서 사용한다. 따라서 주로 상향식 TDD가 된다.
    • 과도한 명세 문제
      • 런던파는 테스트 대역을 사용하기 때문에 stub을 한다. 따라서 구현 세부 사항과 과도하게 결합하는 경향이 있다. 
      • 고전파는 테스트 대역을 사용하지 않는다. 따라서 구현 세부 사항과 결합되는 경향이 다소 감소한다. 

    2.4 두 분파의 통합 테스트

    격리 문제에서 서로 다른 의견을 보이기 때문에 통합 테스트 역시 각 분파는 서로 다른 관점을 가지고 있다.

    런던파는 의존성으로 실제 객체를 이용해서 하는 모든 테스트를 통합 테스트로 의미한다. 즉, 고전파 입장에서 하는 단위 테스트는 런던파 입장에서 바라보면 모두 통합테스트가 된다. 

    고전파는 다음 단위 테스트의 정의 중 하나라도 만족하지 않으면, 그것을 통합테스트로 바라본다. 예를 들어 공유 의존성에 접근하는 테스트는 테스트끼리 격리될 수 없다. 따라서 이런 것들은 결국 통합테스트가 되는 것이다. 

    • 단일 동작 단위를 검증한다.
    • 빠르게 수행한다
    • 테스트끼리 격리되어야 한다. 

    2.4.1 통합 테스트의 일부인 엔드 투 엔드 테스트

    통합 테스트는 다음을 의미한다

    • 공유 의존성, 프로세스 외부 의존성과 통합해 작동하는지 검증한다. 
    • 조직 내 다른팀이 개발한 코드 등과 통합해 작동하는지 검증한다. 

    엔드 투 엔드 테스트라는 것도 존재하는데, 이 역시 통합 테스트의 일종이다. 대신 다음과 같이 세부화 된 의미를 가진다

    • 통합 테스트는 1~2개의 외부 의존성만 포함해서 테스트 한다.
    • 엔드 투 엔드 테스트는 실제 운영환경과 동일한 수준의 의존성을 포함해서 테스트 한다.

    요약

    • 단위 테스트의 정의는 다음과 같다.
      • 단일 동작 단위를 검증한다
      • 빠르게 수행된다
      • 다른 테스트와 별도로 처리한다. 
    • 격리 문제
      • 런던파는 클래스끼리 격리 해야한다고 한다. 테스트 대상을 제외한 모든 의존성을 테스트 대역으로 대체한다. 
      • 고전파는 단위 테스트를 격리 해야한다고 한다. 공유 의존성만 테스트 대역으로 대체한다. 
    • 런던파는 더 나은 입자성, 큰 객체 그래프를 가진 테스트의 용이성, 테스트 후 실패 지점을 빠르게 찾을 수 있는 장점을 가짐. 
    • 객체 그래프가 큰 테스트 코드는 제품 코드의 설계가 잘못되었다는 것을 의미한다. 제품 코드의 추상화를 추가한 후에, 테스트 하기 쉽도록 구조를 바꿔야 한다. 
    • 런던파의 테스트는 구현 세부사항과 강합게 결합되는 문제가 있다. 
    • 통합 테스트는 단위 테스트 기준 중 하나 이상을 만족하지 못하는 테스트다. 
    • 엔드 투 엔드 테스트는 통합 테스트의 일종이다. 통합 테스트가 의존성 1~2개만 포함한다면, 엔드 투 엔드 테스트는 모든 의존성을 포함해서 테스트 한다.

    댓글

    Designed by JB FACTORY