들어가며

테스트 코드 역시 코드이기 때문에 지속적으로 관리가 필요하다. 따라서 무분별하게 테스트 코드를 양산하는 것은 좋지 않다.

최소한의 유지비로 최대 가치를 뽑아내기 위해서는, 가치 있는 테스트의 식별과 작성이 가능해야 한다. 이번 장에서는 가치 있는 테스트를 식별하는 방법을 학습해 보자!

 

좋은 단위 테스트의 4대 요소

1. 회귀 방지

회귀란 소프트웨어 버그를 말한다. 그러니 회귀 방지란, 구현한 로직이나 외부 라이브러리와 같은 프로젝트 요소들이 문제가 없도록 검증하는 것을 의미한다. 테스트가 많은 코드를 실행할수록 회귀 방지 지표가 극대화된다.

 

2. 리팩터링 내성

리팩터링 내성이란, 테스트를 실패로 바꾸지 않으면서 기존 로직을 리팩터링 할 수 있는지에 대한 척도이다. 기능을 이전과 동일하게 동작하도록 수정을 했지만 테스트가 실패하는 것을 거짓 양성이라고 부른다. 이런 거짓 양성이 빈번하다면, 테스트 코드에 대한 신뢰도 저하와 그로 인해 리팩터링을 꺼리게 되는 부작용을 낳는다.

 

이를 피하기 위해서는 테스트가 구현 세부 사항과 결합되지 않도록 주의해야 한다. 예를 들어 어떤 사람이 마트에서 과자를 샀다고 생각해 보자. 리팩터링 내성이 좋은 테스트에서는 과자의 재고가 감소했는지만 검증한다. 어떤 방식(또는 알고리즘)으로 재고가 감소했는지, 재고를 감소하는 메서드가 호출 됐는지는 검증하지 않는다. 이는 구현 세부 사항이다.

 

즉, 리팩터링 내성을 높이는 방법은 테스트의 최종 결과를 목표로 하는 것이다. 이는 곧 테스트를 작성할 때는 블랙박스 테스트가 더 낫다는 것을 의미한다. 유일한 예외는 알고리즘 복잡도가 높은 유틸리티 코드를 다루는 경우다.

 

3. 빠른 피드백

테스트 속도가 빠를수록 테스트 스위트에서 더 많은 테스트를 수행할 수 있고 더 자주 실행할 수 있다. 이는 단위 테스트의 필수 속성이다.

 

4. 유지 보수성

유지비를 평가하는 지표로, 다음 두 가지의 주요 요소로 구성된다.

  • 테스트가 얼마나 이해하기 어려운가 : 테스트는 코드 라인이 적을수록 더 읽기 쉽다. 그렇다고 인위적으로 압축하라는 의미는 아니다.
  • 테스트가 얼마나 실행하기 어려운가 : 테스트가 외부 의존성을 가지고 있을 수 있다. 그런 테스트를 실행하려면 데이터베이스 서버를 재부팅하고 네트워크 연결 문제를 해결하는 등의 절차가 필요하다.

 

이상적인 테스트?

위의 네 가지 특성을 모두 만족시키는 이상적인 테스트는 존재하지 않는다. 왜냐하면 처음 세 가지 특성인 회귀 방지, 리팩터링 내성, 빠른 피드백은 상호 배타적이기 때문이다.(유지 보수성은 독립적으로 극대화할 수 있다!) 여기서 리팩터링 내성은 있거나 없거나 둘 중 하나이기 때문에, 회귀 방지와 빠른 피드백 사이에서 절충해야 한다.

 

단위 테스트를 구성하는 방법

테스트를 준비, 실행, 검증이라는 세 부분으로 나눌 수 있다,

  • 준비(given) : 테스트 대상 시스템(sut, 테스트할 클래스를 의미한다)과 해당 의존성을 원하는 상태로 만든다. 
  • 실행(when) : sut에 준비된 의존성을 전달하고 메서드를 호출하며, 출력이 있으면 출력 값을 캡쳐한다.
  • 검증(then) : 결과를 검증한다. 결과는 반환 값이나 sut와 협력자의 최종 상태, sut가 협력자에 호출한 메서드 등으로 표시될 수 있다.
    • 협력자는 공유하거나 변경 가능한 의존성이다. 값이나 불변 객체는 협력자에 해당하지 않는다.

 

아래와 같은 안티패턴은 주의한다.

  • 여러 개의 준비, 실행, 검증 구절 피하기
  • 테스트 내 if 문 피하기 : 유지보수와 가독성이 어려워진다.

 

테스트 간 중복 코드 제거

  • 준비 구절에서 작성하는 객체 생성 로직은 테스트 간에 반복되는 경향이 있다. 해당 로직을 팩토리 메서드로 추출하면, 테스트 간에 재사용이 가능하다. 이때, 테스트 간에 결합이 되지 않도록 주의한다.
  • 데이터베이스와 작동하는 통합테스트와 같이, 테스트 전부 또는 대부분에 사용되는 로직을 abstract 클래스로 추출해서 재사용할 수 도 있다.

단위 테스트

단위 테스트란 1) 작은 코드 조각을 검증하고, 2) 빠르게 수행하고, 3) 격리된 방식으로 처리하는 자동화된 테스트다. 여기서 작은 코드 조각과 격리된 방식을 어떻게 정의하느냐에 따라 런던파와 고전파로 나뉜다. 

 

가령 `친구가 공을 던진다` 라는 행위를 각 분파의 방식으로 테스트 해보겠다. 런던파는 단일 클래스만을 검증한다. 따라서 공을 목으로 처리하고 친구라는 클래스만 검증한다. 반면, 고전파는 단일 동작을 검증하기 때문에, 친구와 공 모두 실제 클래스를 사용하여 행위를 검증한다. 

 

  격리 주체 단위의 크기 테스트 대역 사용 대상
런던파 단위 단일 클래스 불변 의존성 외 모든 의존성
고전파 단위 테스트 단일 클래스 또는 클래스 세트 공유 의존성

 

위 표에 나와 있듯이 격리 방식에도 차이가 있다. 런던파의 격리 주체는 단일 클래스이기 때문에 친구와 공이 서로 격리되어 있다. 하지만 고전파는 클래스가 아니라 테스트 단위로 격리한다. 성공과 실패를 각각 테스트 한다고 하면, 두 테스트는 서로 간섭하지 말아야 한다는 의미이다. 그러기 위해서는 데이터베이스나 싱글톤 같은 요소가 서로 공유되지 말아야 한다.

 

필자는 고전파 방식이 리팩터링 내성과 테스트 유효성이 높다고 판단하였기 때문에 해당 분파를 지지한다.

 

통합 테스트

통합 테스트는 단위 테스트 정의 중 하나를 충족하지 않는 테스트다. 데이터베이스와 같은 공유 의존성에 접근하거나, 둘 이상의 동작 단위를 테스트하는 것이 이에 해당된다.

 

엔드 투 엔드 테스트

엔드 투 엔드 테스트는 통합 테스트의 일부로, 일반적으로 외부 의존성을 더 많이 포함한다. 보통 통합 테스트는 프로세스 외부 의존성을 한두 개만 갖고 동작한다. 반면에 엔드 투 엔드 테스트는 프로세스 외부 의존성을 전부 또는 대다수 갖고 작동한다. 즉, 모든 외부 애플리케이션을 포함해 시스템을 최종 사용자의 관점에서 검증하는 것을 말한다. 동의어로 UI 테스트, GUI 테스트, 기능 테스트가 있다.

+ Recent posts