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

AAA 패턴 사용

  • AAA 패턴은 각 테스트를 준비(arrange), 실행(act), 검증(assert)이라는 세 부분으로 나눌 수 있다.
    • 준비 구절: 테스트 대상 시스템(SUT)과 해당 의존성을 원하는 상태로 만든다.
    • 실행 구절: SUT에서 메서드를 호출하고 준비된 의존성을 전달하며 출력 값을 캡처한다.
    • 검증: 결과를 검증한다. SUT의 반환 값이나, SUT 협력자의 최종 상태, SUT가 협력자에 호출한 메서드 등으로 결과를 검증할 수 있다.
  • Given-When-Then 패턴과 차이점은 없다. 유일한 차이점은 프로그래머가 아닌 사람에게 Given-When-Then 구조가 더 읽기 쉽다는 것이다.
class Calculator {  
    fun sum(first: Double, second: Double): Double {  
       return first + second  
    }  
}  
  
class CalculatorTest {  
  
    @Test  
    fun sumOfTwoNumbers() {  
       // 준비  
       val first = 10.0  
       val second = 20.0  
       val calculator = Calculator()  
  
       // 실행  
       val result = calculator.sum(first, second)  
  
       // 검증  
       assert(result == 30.0)  
    }  
}
  • AAA 패턴의 일관성은 익숙해지면 모든 테스트를 쉽게 읽을 수 있고 이해할 수 있다.
    • 결국 전체 테스트 스위트의 유지 보수 비용이 줄어든다.
  • 검증 구절로 시작하는 것도 가능한 옵션이다.
    • TDD를 실천할 때, 즉 개발하기 전에 실패할 테스트를 만들 때는 아직 기능이 어떻게 동작할지 충분히 알지 못한다.
    • 따라서 먼저 기대하는 동작으로 윤곽을 잡은 다음, 이러한 기대에 부응하기 위한 시스템을 어떻게개발할지 아는 것이 좋다.
    • 테스트 전에 제품 코드를 작섣ㅇ한다면 테스트를 작성할 시점에 실행에서 무엇을 예상하는지 이미 알고 있으므로 준비 구절부터 시작하는 것이 좋다.

여러 개의 준비, 실행, 검증 구절 피하기

  • 때로는 준비, 실행, 검증 구절이 여러 개 있는 테스트를 만날 수 있다.
  • 이는 더이상 단위 테스트가 아니라 통합 테스트다. 이러한 테스트 구조는 피하는 것이 좋다.
    • 실행이 하나면 테스트가 단위 테스트 범주에 있게끔 보장하고, 간단하고, 빠르며, 이해하기 쉽다.
  • 통합 테스트에서는 실행 구절을 여러 개두는 것이 괜찮을 때도 있다.
    • 테스트의 속도를 높이기 위해 여러 개의 통합 테스트를 여러 실행과 검증이 있는 단일 테스트로 묶는 경우

테스트 내 if 문 피하기

  • if 문이 있는 단위 테스트는 안티 패턴이다.
  • if 문은 테스트가 한 번에 너무 많은 것을 검증한다는 표시다.
  • 통합 테스트도 if문은 피해야된다.
  • 테스트에 분기가 있어서 얻는 이점은 없다. 단지 추가 유지비만 불어난다.
  • if문은 테스트를 읽고 이해하는 것을 더 어렵게 한다.

각 구절은 얼마나 커야 하는가?

  • 준비 구절이 가장 큰 경우
    • 일반적으로 준비 구절이 세 구절 중 가장 크며, 실행과 검증을 합친 만큼 클 수도 있다.
    • 너무 크다면, 같은 테스트 클래스 내 비공개 메서드 또는 팩토리 클래스로 도출하는 것이 좋다.
    • 준비 구절에서 코드 재사용에 도움이 되는 두 가지 패턴으로 오브젝트 마더(Object mother)와 테스트 데이터 빌더(Test Data Builder)가 있다.
  • 실행 구절이 한 줄 이상인 경우 경계하라
    • 실행 구절이 두 줄 이상인 경우 SUT의 공개 API에 문제가 있을 수 있다.
    • 캡슐화를 지켜서, 잠재적 모순으로 부터 코드를 보호해야 된다.

검증 구절에는 검증문이 얼마나 있어야 하는가

  • 단위 테스트의 단위는 동작의 단위이지 코드의 단위가 아니다. 단일 동작 단위는 여러 결과를 낼 수 있으며, 하나의 테스트로 그 모든 결과를 평가하는 것이 좋다.
  • 그래도 검증 구절이 너무 커지는 것은 경계해야 한다.
    • SUT에서 반환된 객체 내에서 모든 속성을 검증하는 대신 객체 클래스 내에 적절한 동등 멤버를 정의하는 것이 좋다. 그러면 단일 검증문으로 객체를 기대값과 비교할 수 있다.

종료 단계는 어떤가

  • 종료는 일반적으로 별도의 메서드로 도출돼, 클래스 내 모든 테스트에서 재사용된다.
  • AAA 패턴에서는 이 단계를 포함하지 않는다.
  • 대부분의 단위 테스트는 종료 구절이 필요없다.
  • 종료는 통합 테스트의 영역이다.

테스트 대상 시스템 구별하기

  • SUT는 테스트에서 중요한 역할을 하는데, 애플리케이션에서 호출하고자 하는 동작에 대한 진입점을 제공한다.
    • 동작은 여러 클래스에 걸쳐 있을 만큼 클 수도 있다. 그러나 진입점은 오직 하나만 존재할 수 있다.
  • SUT를 의존성과 구분하는 것이 중요하다. 그렇게 하기 위해서 테스트 내 SUT 이름을 sut로 하는 방법이 있다.

준비 ,실행, 검증 주석 제거하기

  • 빈 줄로 구절을 구분하면 대부분의 단위 테스트에서 효과적이며, 간결성과 가독성 사이에서 균형을 잡을 수 있다.
  • 대규모 테스트에서는 준비 단게에 빈 줄을 추가해 설정 단계를 구분할 수도 있다.
    • 통합 테스트에는 복잡한 설정을 포함ㅎ는 경우가 많다.
  • AAA 패턴을 따르고 준비 및 검증 구절에 빈 줄을 추가하지 않아도 되는 테스트라면 구절 주석들을 제거하라
  • 그렇지 않으면 구절 주석을 유지하라

테스트 간 테스트 픽스처 재사용

  • 준비 구절에서 코드를 재사용하는 것이 테스트를 줄이면서 단순화하기 좋은 방법이다.
    • 이러한 준비는 별도의 메서드나 클래스로 도출한 후 테스트 간에 재사용하는 것이 좋다.
  • 테스트 픽스처: 테스트 실행 대상 객체다. 이 객체는 SUT로 전달되는 인수다.
    • 테스트 실행 전에 알려진 고정 상태로 유지하기 때문에 동일한 결과를 생성한다. 따라서 픽스처라는 단어가 나왔다.

테스트 픽스처를 재사용하는 잘못된 방법

  • @BeforeClass 등에 픽스처를 초기화하는 방법
    • 테스트 간 결합도가 높아진다.
    • 테스트 가독성이 떨어진다.
  • 테스트 간의 높은 결합도는 안티 패턴이다.
    • 모든 테스트가 서로 결합돼 있다.
    • 테스트를 수정해도 다른 테스트에 영향을 주어서는 안된다.
  • 테스트 가독성을 떨어뜨리는 생성자 사용
    • 테스트만 보고는 더 이상 전체 그림을 볼 수 없다.
    • 테스트 메서드가 무엇을 하는지 이해하려면 클래스의 다른 부분도 봐야 한다.

더 나은 테스트 픽스처 재사용법

  • 테스트 클래스에 비공개 팩토리 메서드를 두는 방법
  • 공통 초기화 코드를 비공개 팩토리 메서드로 추출해 테스트 코드를 짧게 하면서, 동시에 테스트 진행 상황에 대한 전체 맥락을 유지할 수 있다.
  • 비공개 메서드를 충분히 일반화하는 한 테스트가 서로 결합되지 않는다.
  • 테스트 픽스처 재사용 규칙 한 가지 예외: 테스트 전부 또는 대부분에 사용되는 픽스처는 생성자에 인스턴스화할 수 있다.
    • 데이터베이스 연결 등
    • base class를 만들어 개별 테스트 클래스가 아니라 base 클래스 생성자에서 초기화 하는 것이 더 합리적이다.

단위 테스트 명명법

  • 아래는 단위 테스트를 명명하는 가장 유명하지만 가장 도움이 되지 않는 방법이다.
    • [테스트 대상 메서드]_[시나리오]_[예상 결과]
    • 테스트 대상 메서드: 테스트 중인 메서드의 이름
    • 시나리오: 메서드를 테스트하는 조건
    • 예상 결과: 현재 시나리오에서 테스트 대상 메서드에 기대하는 것
    • 동작 대신 구현 세부 사항에 집중하게끔 부추기기 때문에 분명히 도움이 되지 않는다.

단위 테스트 명명 지침

  • 표현력 있고 읽기 쉬운 테스트 이름을 지으려면 다음 지침을 따르자.
    • 엄격한 명명 정책을 따르지 않는다. 복잡한 동작에 대한 높은 수준의 설명을 이러한 정책의 좁은 상자 안에 넣을 수 없다. 표현의 자유를 허용하자.
    • 문제 도메인에 익숙한 비개발자들에게 시나리오를 설명하는 것처럼 테스트 이름을 짓자.
    • 단어를 밑줄(_)표시로 구분한다. 그러면 긴 이름에서 가독성을 향상 시키는데 도움이 된다.

예제: 지침에 따른 테스트 이름 변경

  • 테스트 이름에 SUT의 메서드 이름을 포함하지 말라.
    • 코드의 구현 세부 사항과 테스트 간의 결합도가 높아진다. 이는 테스트 스위트의 유지 보수성에 부정적인 영향을 미친다.
  • should be 문구는 또 다른 일반적인 안티 패턴이다. 사실을 서술할 때는 소망이나 욕구가 들어가지 않는다.
  • 테스트를 완벽하게 읽을 수 있도록 기초 영문법을 지켜야 한다.(‘a’ 같은 관사)

매개변수화된 테스트 리팩터링하기

  • 매개변수화된 테스트를 사용하면 테스트 코드 양을 크게 줄일 수 있지만, 테스트 메서드가 나타내는 사실을 파악하기 어려워진다.
    • 절충안으로 긍정적인 테스트 케이스는 고유한 테스트로 도출하고, 가장 중요한 부분을 잘 설명하는 이름을 쓰면 좋다.
  • 경험상 입력매개변수만으로 테스트 케이스를 판단할 수 있다면 긍정적인 테스트 케이스와 부정적인 테스트 케이스 모두 하나의 메서드로 두는 것이 좋다.

매개변수화된 테스트를 위한 데이터 생성

  • xUnit에는 테스트 메서드에 공급할 사용자 정의 데이터를 생성하는 데 사용할 수 있는 기능이 있다.

검증문 라이브러리를 사용한 테스트 가독성 향상

  • 검증문 라이브러리를 사용하는 이점: 검증문을 재구성해 가독성을 높인다.
  • 유일한 단점은 프로젝트에 의존성을 추가해야 한다.