요약

해당 장에서는

  • 단위테스트 구조(AAA패턴)
  • 좋은 단위테스트 명명법
  • 매개변수화된 테스트(Parameterized Test)

과 같은 내용을 배운다.

단위테스트 구조 AAA패턴은 우리가 일반적으로 테스트를 작성하는 Given-When-Then 구조인 BDD와 굉장히 유사하다.

좋은 단위테스트 명명법은 기초적인 문법을 준수해야 하며 개발자가 아닌 다른 사람이 읽어도 이해할 수 있게 짓어야 한다.

그리고 매개변수화된 테스트를 통해 유사하게 작성된 테스트를 여러 개로 묶을 수 있는 기능을 제공한다.

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

AAA(Arrange, Act, Assert) 패턴 사용

AAA 패턴은 각 테스트를 준비, 실행, 검증이라는 부분으로 나눈다.

  1. 준비 구절에서 SUT과 해당 의존성을 원하는 상태로 만든다.
  2. 실행 구절에서는 SUT에서 메서드를 호출하고 준비된 의존성을 전달하며 (출력이 있다면) 출력값을 캡처한다.
  3. 검증 구절에서는 결과를 검증한다.
    결과는 반환 값이나 SUT와 협력자의 최종 상태, SUT가 협력자에 호출한 메서드 등으로 표시될 수 있다.
class Calculator{
    fun sum(first: Double, second: Double): Double 
        = second + second
}

internal class CalculatorTests{

    @Test
    fun void `Sum of two numbers`(){
        // 준비(Arrange)
        val first = 10
        val second = 20
        val calculator = Calculator()

        // 실행(Act)
        val result = calculator.sum(first, second)

        // 검증(Assert)
        Assertions.assertEquals(30, result)
    }
}

AAA 패턴은 스위트 내 모든 테스트가 단순하고 균일한 구조를 갖는 데 도움이 된다. ->결국 전체 테스트 스위트의 유지 보수 비용이 줄어든다.

피해야 할 것

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

여러 개의 동작 단위를 검증하는 테스트를 의미하게 되고 이러한 테스트는 더 이상 단위 테스트가 아니라 통합테스트이다.

실행이 하나면 테스트가 단위 테스트 범주에 있게끔 보장하고 간단하고, 빠르며, 이해하기 쉽다.

2. 테스트 내 if 문 피하기

if 문은 테스트가 한 번에 너무 많은 것을 검증한다는 표시다. 즉, 테스트를 여러 개로 나눌 수 있다.

통합테스트에도 분기가 늘어나 얻는 이점이 없다.

AAA패턴의 각 구절의 크기

  • 일반적으로 준비 구절이 가장 크다.
    • 훨씬 크면 테스트 내 비공개 메서드 or 별도의 팩토리 클래스로 도출하는 게 좋다.
      • Object Mother, Test Data Builder
  • 실행 구절은 보통 코드 한줄이다.
    • 실행 구절이 두 줄 이상이라면 SUT의 공개 API에 문제가 있을 수 있다.
  • 단일 동작 단위는 여러 결과를 낼 수 있어 하나의 테스트로 그 모든 결과를 평가해도 좋다
    • 하지만, 검증 구절이 너무 커지는 것은 제품 코드에서 추상화가 누락될 수 있다는 의미다.

SUT 구별하기

SUT는 애플리케이션에서 호출하고 하는 동작에 대한 진입점을 제공한다.

그러나 진입점은 오직 하나만 존재할 수 있다.

그러므로 SUT를 의존성과 구분하는 것이 중요하다.

internal class CalculatorTests{

    @Test
    fun void `Sum of two numbers`(){
        // 준비(Arrange)
        val first = 10
        val second = 20
        val sut = Calculator()

        // 실행(Act)
        val result = sut.sum(first, second)

        // 검증(Assert)
        Assertions.assertEquals(30, result)sut
    }
}

이처럼 테스트 내 SUT 변수명을 sut로 구분하면 자연스럽게 구분이 된다.

추가로

  • 테스트 시 준비 구절부터 시작하는게 아닌 검증 구절부터 시작하는 방법도 있다. (TDD를 실천할 때)
  • AAA패턴은 Given-When-Then패턴과 차이는 없지만, 프로그래머가 아닌 사람에게 더 읽기 쉬운 구조이다.

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

테스트 픽스처는 테스트 실행 대상 객체다.

  • DB나 하드 디스크의 파일일 수 있다.
  • 이러한 객체는 테스트 실행 전에 알려진 고정 상태로 유지하기 때문에 동일한 결과를 생성한다.

올바르지 않은 테스트 간 테스트 픽스처 재사용

테스트 간 높은 결합도는 안티패턴

준비 구절을 테스트 간 공유하게 된다면 모든 테스트가 서로 결합되고 만약 테스트의 준비 로직을 수정하면 클래스 속 모든 테스트에 영향을 미친다.

테스트 가독성을 떨어뜨리는 생성자 사용

준비 코드를 생성자로 추출할 때 단점은 테스트 가독성을 떨어뜨리는 것이다.

  • 테스트가 무엇을 하는지 이해하려면 클래스의 다른 부분을 봐야한다.

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

  • 비공개 팩토리 메서드

3. 단위 테스트 명명법

  • 엄격한 명명 정책을 따르지 않는다.
  • 문제 도메인에 익숙한 비개발자들에게 시나리오를 설명하는 것처럼 테스트 이름을 짓자
  • 단어를 밑줄(_) 표시로 구분한다.
    • Kotlin에서는 백틱(`)으로 함수 이름을 감싸면 띄어쓰기를 사용할 수 있다.

테스트 클래스 이름을 지을 때 [클래스명]Tests 패턴을 사용하지만 테스트가 해당 클래스만 검증하는 것으로 제한하는게 아니다. 단위 테스트에서 단위는 동작의 단위지 클래스의 단위가 아니다.

테스트 이름 명명시 피해야 하는 사항

  • 테스트 이름에 SUT의 매서드 이름을 포함하지 않는다.
  • 사실을 서술할 때 소망이나 욕구가 들어가지 않는다. (ex. should be 문구)

4. 매개변수화된 테스트

보통 테스트 하나로는 동작 단위를 완전하게 설명하기 충분하지 않다.

동작이 충분히 복잡하면 이를 설명하는 데 테스트 수가 급격히 증가할 수 있으며 관리하기 어려워질 수 있다.

대부분의 단위 테스트 프레임워크는 유사한 테스트를 묶을 수 있는 매개변수화된 테스트 기능을 제공한다.

예시

아래와 같이 가장 빠른 배송일이 오늘로부터 이틀 후가 되도록 작동하는 배송기능이 있다면

fun `Deliverty for today is invalid`()
fun `Deliverty for tomorrow is invalid`()
fun `The soonest delivery date is two days from now`()
@ParameterizedTest
@CsvSource(
    "HECTO_FINANCIAL,CARD,1000",
    "NONE,MAINTENANCE_FEE,2000",
    "NONE,ZERO,0",
)
fun `Can detect an invalid delivery date`(
    daysFromNow: Int,
    expected: Boolean
)