2장, 단위 테스트란 무엇인가

2022.03.08

단위 테스트의 정의에는 놀랄 만큼 많은 뉘앙스가 있다. 해석의 차이가 생겼고, 단위 테스트에 접근하는 방법이 두가지 견해로 나뉘었다.

  • 고전파, Classical school
    • 모든 사람이 단위 테스트와 테스트 주도 개발에 원론적으로 접근.
  • 런던파, London school
    • 런던의 프로그래밍 커뮤니티에 시작되었고 고전파와는 다른 해석으로 접근.

2.1 ‘단위 테스트’의 정의

단위 테스트에는 많은 정의가 있다. 가장 중요한 세가지 속성은 아래와 같다.

  • 작은 코드 조각(단위라고도 함)을 검증하고,
  • 빠르게 수행하고,
  • 격리된 방식으로 처리하는 자동화된 테스트다.

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

런던파에서는 테스트 대상 시스템을 협력자(collaborator)에게서 격리하는 것을 일컫는다.
즉, 하나의 클래스가 다른 클래스 또는 여러 클래스에 의존하면 이 모든 의존성을 테스트 대역(test double)으로 대체해야 한다.

테스트 대역은 복잡성을 줄이고 테스트를 용이하게 하는 단순화된 버전이다.
제라드 메스자로스가 그의 저서 xUnit 테스트 패턴에서 이 용어를 처음 소개.
> 스턴드 대역이라는 개념에서 유래되었다.

테스트 대역을 사용하여 얻는 이점.

  • 테스트가 실패하면 코드베이스의 어느 부분이 고장 났는지 확실히 알 수 있다는 것이다.
  • 테스트 대상이 가지고 있는 직접적인 의존성을 대체할 수 있고, 더 나아가 의존성의 의존성을 다룰 필요도 없다.
  • 한 번에 한 클래스만 테스트하라는 지침을 도입하면 전체 단위 테스트 스위트를 간단한 구조로 할 수 있다.

테스트 대역은 실행과 관련 없이 모든 종류의 가짜 의존성을 설명하는 포괄적인 용어다.
목은 그러한 의존성의 한 종류일 뿐이다.

런던 스타일은 테스트 대역(목)으로 테스트 대상 코드 조각을 분리해서 격리 요구 사항에 다가간다.

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

단위 테스트의 격리 방식 접근에 있어 고전적인 방법은 코드를 꼭 격리하는 방식으로 하지 않아도 된다.
하지만 단위 테스트는 서로 격리해서 실행해야 한다.
각각의 테스트를 격리하는 것은 여러 클래스가 모두 메모리에 상주하고 공유 상태에 도달하지 않는 한, 여러 클래스를 한 번에 테스트해도 괜찮다는 뜻이다.
데이터베이스, 파일 시스템 등 프로세스 외부 의존성이 공유 상태의 대표적 예다.

고전적인 접근은 테스트 대역 사용이 훨씬 더 평범한 견해를 수반한다.

  • 테스트 간에 공유 상태를 일으키는 의존성에 대해서만 테스트 대역을 사용.

공유 의존성, Shared dependency
공유 의존성은 테스트 간에 공유되고 서로의 결과에 영향을 미칠 수 있는 수단을 제공하는 의존성.
전형적인 예는 정적 가변 필드로 동일한 프로세스 내에서 실행되는 모든 단위 테스트에서 볼 수 있다.

비공개 의존성, Private dependency
공유하지 않는 의존성

프로세스 외부 의존성, out-of-process dependency
애플리케이션 실행 프로세스 외부에서 실행되는 의존성이며, 아직 메모리에 없는 데이터에 대한 프록시다.
프로세스 외부 의존성은 대부분 공유 의존성에 해당하지만 모두 그런 것은 아니다.
데이터베이스는 프로세스 외부 의존성이면서 공유 의존성이지만, 각 테스트 실행 전에 도커 컨테이너로 데이터베이스를 시작하면 테스트가 더 이상 동일한 인스턴스로 작동하지 않기 때문에 프로세스 외부이면서 비공개 의존성이 된다. 실제 프로젝트에서는 프로세스 외부 의존성가 아닌 공유 의존성은 거의 없다.

공유 의존성은 테스트 대상 클래스(단위) 간이 아니라 단위 테스트 간에 공유한다.
그런 의미에서 싱글턴 의존성은 각 테스트에서 새 인스턴스를 만들 수 있기만 하면 공유되지 않는다.
제품 코드에서는 싱글턴 인스턴스가 단 하나만 있지만, 테스트는 이 패턴을 따르지 않고 재사용하지도 않는다. 따라서 이러한 의존성은 비공개인 것이다.

휘발성 의존성, volatile dependency
휘발성 의존성은 다음 속성 중 하나를 나타내는 의존성이다.

  • 개발자 머신에 기본 설치된 환경 외에 런타임 환경의 설정 및 구성을 요구한다. ex) 데이터베이스와 API 서비스
  • 비결정적 동작을 포함한다. ex) 난수 생성기, 현재 날짜와 시간을 반환하는 클래스. 공유 의존성과 휘발성 의존성은 겹치는 부분이 있다.

공유 의존성을 대체하는 또 다른 이유는 테스트 실행 속도를 높이는 데 있다.
공유 의존성은 거의 항상 실행 프로세스 외부에 있는 데 반해, 비공개 의존성은 보통 그 경계를 넘지 않는다.
따라서, 공유 의존성에 대한 호출은 비공개 의존성에 대한 호출보다 더 오래 걸린다.

그리고 단위 테스트의 속성으로 빨리 실행해야 하는 필요성이 있으므로, 공유 의존성을 가진 테스트는 단위 테스트 영역에서 통합 테스트 영역으로 넘어간다.

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

런던파와 고전파로 나눠진 원인은 격리 특성에 있다.
런던파는 테스트 대상 시스템에서 협력자를 격리하는 것으로 보는 반면, 고전파는 단위 테스트끼리 격리하는 것으로 본다.

런던파와 고전파는 아래와 같은 의견 차이가 있다.

  • 격리 요구 사항
  • 테스트 대상 코드 조각(단위)의 구성 요소
  • 의존성 처리
격리 주체단위의 크기테스트 대역 사용 대상
런던파단위단일 클래스불변 의존성 외 모든 의존성
고전파단위 테스트단일 클래스 또는 클래스 세트공유 의존성

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

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

테스트 대역을 어디에서나 흔히 사용할 수 있지만, 런던파는 테스트에서 일부 의존성을 그대로 사용할 수 있도록 하고 있다.
**의존성의 변경 가능 여부. 즉, 불변 객체는 교체하지 않아도 된다.**
var storeMock = new StoreMock<IStore>();

bool success = customer.Purchase(storeMock.Object, Product.Shampoo, 5);

위의 예제에서 Store는 시간에 따라 변할 수 있는 내부 상태를 포함하고 있다.
하지만 Product.Shampoo (열거형)과 숫자 5는 불변 객체(또는 값)이므로 테스트 대역을 사용하지 않았다.

고전파에서는 공유 의존성만을 테스트 대역으로 교체하지만, 런던파에서는 변경 가능한 비공개 의존성도 테스트 대역으로 교체할 수 있다.

공유 의존성은 거의 항상 프로세스 외부에 있지만, 모든 프로세스 외부 의존성은 공유 의존성의 범주에 속하지는 않는다.
프로세스 외부 의존성을 공유하려면 단위 테스트가 서로 통신할 수 있는 수단이 있어야 한다. 의존성 내부 상태를 수정하면 통신이 이뤄진다.
그런 의미에서 프로세스 외부의 불변 의존성은 그런 수단을 제공하지 않는다. 테스트는 내부의 어떤 것도 수정할 수 없기 때문에 서로 실행 컨텍스트 영향을 줄 수 없다.

그렇다고 프로세스 외부의 불변 의존성을 테스트 범주에 포함해야하는 것은 아니다. 테스트 속도를 높이려면 테스트 대역으로 교체해야 한다.
그러나 프로세스 외부 의존성이 충분히 빠르고 안정적이면 테스트에서 그대로 사용하는 것도 괜찮다.

2.3 고전파와 런던파의 비교

고전파와 런던파 간의 주요 차이는 단위 테스트의 정의에서 격리 문제 어떻게 다루는지에 있다.
이는 결국 테스트해야 할 단위의 처리와 의존성 취급에 대한 방법으로 넘어간다.

런던파의 접근 방식은 다음과 같은 이점이 있다.

  • 입자성(granularity)이 좋다.
    • 테스트가 세밀해서 한 번에 한 클래스만 확인.
  • 서로 연결된 클래스의 그래프가 커져도 테스트하기 쉽다.
    • 모든 협력자는 대역으로 대체.
  • 테스트가 실패하면 어떤 기능이 실패했는지 확실히 알 수 있다.

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

좋은 입자성에 관한 요점은 단위 테스트에서 단위를 구성하는것에 대한 논쟁과 관련이 있다.
객체지향 개발자들은 보통 클래스를 코드베이스의 기초에 위치한 원자 빌딩 블록으로 간주하여, 클래스를 테스트에서 검증할 원자 단위로 취급하게 된다.
여기에는 오해의 소지가 있다.

테스트는 코드의 단위를 검증해서는 안 된다.
오히려 동작의 단위, 즉 문제 영역에 의미가 있는 것. 이상적으로는 비지니스 담당자가 유용하다고 인식할 수 있는 것을 검증해야 한다.

그래서 좋은 코드 입자성을 목표로 하는 것은 도움이 되지 않는다. 테스트가 단일 동작 단위를 검증하는 한 좋은 테스트다.

테스트는 해결하는 데 도움이 되는 문제애 대한 이야기를 들려줘야 하며, 이 이야기는 프로그래머가 아닌 일반 사람들에게 응집도가 높고 의미가 있어야 한다.

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

실제 협력자를 대신해 목을 사용하면 클래스를 쉽게 테스트할 수 있다. ( 여러 계층에 걸쳐 의존성 그래프가 복잡할 수록 더 쉬워진다. )

상호 연결된 클래스의 크고 복잡한 그래프를 찾는 대신, 먼저 이러한 클래스 그래프를 갖지 않는데 집중해야 한다.
대게 클래스 그래프가 커진 것은 코드 설계 문제의 결과다.

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

런던 스타일 테스트가 있는 시스템에 버그가 생기면, 보통 대상에 버그가 포함된 테스트만 실패한다.
하지만 고전적이 방식이면, 오작동하는 클래스를 참조하는 클라이언트를 대상으로 하는 테스트도 실패 할 수 있다.
문제의 원인을 찾기가 더 어려워진다고 생각하지만, 큰 문제는 아니다.
테스트를 정기적으로(이상적으로는 소스 코드가 변경될 때마다) 실행하면 버그의 원인을 알아 낼 수 있다.
또한, 실패한 테스트를 모두 볼 필요도 없으며 하나를 고치면 다른 것들도 자동으로 고쳐진다.

버그가 테스트 하나뿐만 아니라 많은 테스트에서 결함으로 이어진다면, 방금 고장 낸 코드 조각이 큰 가치가 있다는 것을 보여준다.

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

고전파와 런던파 사이에 남아있는 두 가지 차이점은 아래와 같다.

  • 테스트 주도 개발을 통한 시스팀 설계 방식
  • 과도한 명세 문제

테스트 주도 개발 프로세스

  1. 추가해야 할 기능과 어떻게 동작해야 하는지를 나타내는 실패 테스트 작성.
  2. 테스트가 통과할 만큼 충분히 코드 작성.
  3. 테스트 보호하에서 코드 리팩터링.

테스트 주도 개발을 통한 시스팀 설계 방식

런던 스타일의 단위 테스트는 하향식 TDD로 이루어진다.

  1. 전체 시스템에 대한 기대치를 설정하는 상위 레벨 테스트부터 시작한다.
  2. 목을 사용해 예상 결과를 달성하고자 시스템이 통신해야 하는 협력자를 지정.
  3. 모든 클래스를 구현할 때까지 클래스 그래프를 다져나감

목은 한 번에 한 클래스에 집중할 수 있기 때문에 이 설계 프로세스를 가능하게 한다.

고전파는 테스트에서 실제 객체를 다뤄야 하기 때문에 지침을 똑같이 두지 않는다. 대신 일반적으로 상향식으로 한다.

과도한 명세

고전파와 런던파 간의 가장 중요한 차이점은 과도한 명세 문제, 즉 테스트가 대상의 구현 세부 사항에 결합되는 것이다.
런던 스타일은 고전 스타일보다 테스트가 구현에 더 자주 결합되는 편이다.

2.4 두 분파의 통합 테스트

런던파는 실제 협력자 객체를 사용하는 모든 테스트를 통합 테스트로 간주한다.
고전 스타일로 작성된 대부분의 테스트는 런던파 지지자들에게 통합테스트로 느껴질 것이다.

다시 한 번 단위 테스트를 정의하면,

런던파고전파
작은 코드 조각을 검증하고
빠르게 수행하고
격리된 방식으로 처리한다.
단일 동작 단위를 검증하고
빠르게 수행하고
다른 테스트와 별도로 처리한다.

통합 테스트는 이러한 기준 중 하나를 충족하지 않는 테스트다.
예를 들어 공유 의존성에 접근하는 테스트는 다른 테스트와 분리해 실행할 수는 없다.
어떤 테스트에서 데이터베이스 상태 변경이 생기면 병렬로 실행할 때 동일한 데이터베이스에 의존하는 다른 모든 테스트의 결과가 변경될 것이다.
이러한 테스트는 순차적으로 실행해서 각 테스트가 공유 의존성과 함께 작동하려고 기다릴 수 있다.

둘 이상의 동작 단위를 검증할 때의 테스트는 통합테스트다.
다른 동작 단위를 검증하는 느린 테스트가 두 개 있을 때, 하나로 합치는 것이 타당할 수 있다.

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

간단히 말해 통합 테스트는 공유 의존성, 프로세스 외부 의존성뿐 아니라 조직 내 다른 팀이 개발한 코드 등과 통합해 작동하는지도 검증하는 테스트다.
엔드 투 엔드 테스트는 통합 테스트의 일부다.
통합 테스트와 엔드 투 엔드 테스트의 차이점은 일반적으로 엔드 투 엔드 테스트가 의존성을 더 많이 포함한다는 것이다.

가끔 경계가 흐리지만, 통합 테스트는 프로세스 외부 의존성을 한두 개만 갖고 작동한다.
엔드 투 엔드 테스트는 프로세스 외부 의존성을 전부 또는 대다수 갖고 작동한다.

엔드 투 엔드라는 명칭은 모든 외부 애플리케이션을 포함해 시스템을 최종 사용자의 관점에서 검증하는 것을 의미한다.

엔드 투 엔드 테스트를 하더라도 모든 프로세스 외부 의존성을 처리하지 못할 수도 있다.
일부 의존성의 테스트 버전이 없거나 해당 의존성을 필요한 상태로 자동으로 가져오는 것이 불가능할 수 있다.
여전히 테스트 대역을 사용할 필요가 있고, 통합 테스트와 엔드 투 엔드 테스트 사이에 뚜렷한 경계가 없다는 사실을 강조한다.

요약

  • 단위 테스트에 접근하는 방식에 따라 두 가지 견해로 크게 나뉜다.
  • 단위 테스트의 정의.
    • 단일 동작 단위를 검증하고
    • 빠르게 수행하고
    • 다른 테스트와 별도로 처리한다.
  • 테스트는 코드 단위가 아니라 동작 단위를 검증해야 한다.
  • 통합 테스트는 단위 테스트 기준 중 하나 이상을 충족하지 못하는 테스트다.
  • 엔드 투 엔드 테스트는 애플리케이션과 함께 작동하는 프로세스 외부 의존성의 전부 또는 대부분에 직접 접근한다.