[유닛테스트] 3. 단위테스트 구조
요약
해당 장에서는
- 단위테스트 구조(AAA패턴)
- 좋은 단위테스트 명명법
- 매개변수화된 테스트(Parameterized Test)
과 같은 내용을 배운다.
단위테스트 구조 AAA패턴은 우리가 일반적으로 테스트를 작성하는 Given-When-Then 구조인 BDD와 굉장히 유사하다.
좋은 단위테스트 명명법은 기초적인 문법을 준수해야 하며 개발자가 아닌 다른 사람이 읽어도 이해할 수 있게 짓어야 한다.
그리고 매개변수화된 테스트를 통해 유사하게 작성된 테스트를 여러 개로 묶을 수 있는 기능을 제공한다.
1. 단위테스트를 구성하는 방법
AAA(Arrange, Act, Assert) 패턴 사용
AAA 패턴은 각 테스트를 준비, 실행, 검증이라는 부분으로 나눈다.
- 준비 구절에서 SUT과 해당 의존성을 원하는 상태로 만든다.
- 실행 구절에서는 SUT에서 메서드를 호출하고 준비된 의존성을 전달하며 (출력이 있다면) 출력값을 캡처한다.
- 검증 구절에서는 결과를 검증한다.
결과는 반환 값이나 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
- 훨씬 크면 테스트 내 비공개 메서드 or 별도의 팩토리 클래스로 도출하는 게 좋다.
- 실행 구절은 보통 코드 한줄이다.
- 실행 구절이 두 줄 이상이라면 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
)