AAA 패턴은 각 테스트를 준비, 실행, 검증이라는 세 부분으로 나눌 수 있다.
예시
public class Calculator {
public double sum(double first, double second) {
return first + second;
}
}
public class CalculatorTest {
@DisplayName("두 수를 더한다")
@Test
void test() {
// 준비
double first = 10;
double second = 20;
Calculator calculator = new Calculator();
// 실행
double result = calculator.sum(first, second);
// 검증
Assertions.assertThat(result).isEqualTo(30);
}
}
AAA 패턴을 사용하면 모든 테스트가 단순하고 균일한 구조를 가지지게 된다.
이러한 일관성이 이 패턴의 가장 큰 장점 중 하나다.
모든 테스트를 쉽게 읽을 수 있고 이해할 수 있다.
결국 유지 보수 비용을 줄일 수 있다.
Given-When-Then 패턴
AAA와 유사한 Given-When-Then 패턴도 세 부분으로 나뉜다.
패턴 사이에 차이는 없지만 Given-When-Then 패턴이 프로그래머가 아닌 사람이 읽기 더 쉽다.
- Given - 준비 구절
- When - 실행 구절
- Then - 검증 구절
때로는 준비, 실행, 검증 구절이 여러 개 있는 테스트를 만날 수 있다.
예시)
public class CalculatorTest {
@DisplayName("두 수를 더한다")
@Test
void test() {
// 준비
double first = 10;
double second = 20;
Calculator calculator = new Calculator();
// 실행
double result = calculator.sum(first, second);
// 검증
Assertions.assertThat(result).isEqualTo(30);
// 준비
double first = 30;
double second = 10;
// 실행
calculator.divide(first, second);
// 검증
Assertions.assertThat(result).isEqualTo(3);
}
}
검증 구절로 구분된 여러 개의 실행 구절을 보면 여러 개의 동작 단위를 검증하는 테스트를 뜻한다.
이러한 테스트는 단위 테스트가 아니라 통합 테스트이다.
이러한 테스트 구조는 피하는 것이 좋다.
실행이 하나면 테스트가 단위 테스트 범주에 있게끔 보장하고 간단하고, 빠르며, 이해하기 쉽다.
각 동작을 고유의 테스트로 도출해야 한다.
if 문이 단위 테스트도 안티 패턴이다.
if 문은 테스트가 한 번에 너무 많은 것을 검증한다는 표시다.
테스트에 분기가 있어서 얻는 이점은 없고 if문은 테스트를 읽고 이해하기 어렵게 만들어 유지비만 늘어난다.
단위 테스트든 테스트는 분기가 없는 간단한 일련의 단계여야 한다.
그러므로 반드시 여러 테스트로 나눠야 한다.
일반적으로 준비 구절이 세 구절 중 가장 크며 실행과 검증을 합친 만큼 클 수도 있다.
그러나 이보다 훨씬 크면 같은 테스트 클래스 내 private 메서드 또는 별도의 팩토리 클래스로 도출하는 것이 좋다.
테스트당 하나의 검증을 갖는 지침이 있다.
이 지침은 가능한 가장 작은 코드를 목표로 하는 전재에 기반을 두고 있다.
하지만 단위 테스트의 단위는 동작의 단위이지 코드의 단위가 아니다.
단일 동작 단위는 여러 결과를 낼 수 있으며 하나의 테스트로 그 모든 결과를 평가하는 것이 좋다.
테스트에 너무 많은 객체가 생성되어 테스트 대상을 구별하기 힘들 수 있다.
테스트 대상을 명확히 알 수 있게 네이밍을 해야 한다.
예시)
public class CalculatorTest {
@DisplayName("두 수를 더한다")
@Test
void test() {
// 준비
double first = 10;
double second = 20;
Calculator calculator = new Calculator();
// 실행
// 테스트 대상이라는 것을 나타낼 수 있게 actual로 네이밍
double actual = calculator.sum(first, second);
// 검증
Assertions.assertThat(actual).isEqualTo(30);
}
}
테스트 내에서 특정 부분이 어떤 구절에 속해 있는지 파악하는데 시간을 많이 들이지 않도록 세 구절을 서로 구분하는 것은 중요하다.
이를 위한 방법은 주석을 다는 것이다.
하지만 AAA패턴은 구조가 정해졍있기 때문에 불필요하고 테스트의 간결성을 떨어뜨린다.
복잡한 테스트는 주석을 달아도 좋지만 간결한 테스트는 주석을 다는 대신 빈 줄로 구분하자.
준비 구절에서 코드를 재사용하는 것이 테스트를 줄이면서 단순화하기 좋은 방법이다.
이러한 준비는 별도의 메서드나 클래스로 도출한 후 테스트 간에 재사용하는 것이 좋다.
public class CalculatorTest {
private int first = 10;
private int second = 20;
private Calculator calculator = new Calculator();
@DisplayName("두 수를 더한다")
@Test
void test() {
//실행
double actual = calculator.sum(first, second);
// 검증
Assertions.assertThat(actual).isEqualTo(30);
}
}
테스트 코드 양을 크게 줄일 수 있었지만 이 방법에는 단점이 있다.
Junit에서는 다음과 같이 해결할 수 있다.
public class CalculatorTest {
private int first;
private int second;
private Calculator calculator;
@BeforeEach
void setup() {
first = 10;
second = 10;
calculator = new Calculator();
}
@DisplayName("두 수를 더한다")
@Test
void test() {
//실행
double actual = calculator.sum(first, second);
// 검증
Assertions.assertThat(actual).isEqualTo(30);
}
}
Junit5에서 제공하는 @BeforEach는 테스트를 실행하기 전 실행되기 때문에 각 테스트의 결합도를 낮출 수 있다.
private static DefaultFrame createDefaultFrame(int... ints) {
DefaultFrame defaultFrame = new DefaultFrame();
Arrays.stream(ints).forEach(i -> defaultFrame.addScore(Score.of(i)));
return defaultFrame;
}
@DisplayName("첫 번째 점수와 두 번째 점수의 합이 10을 넘으면 IllegalArgumentException 예외를 throw 한다.")
@Test
void validate_sum() {
DefaultFrame defaultFrame = createDefaultFrame(5);
assertThatThrownBy(() -> defaultFrame.addScore(Score.of(6))).isInstanceOf(IllegalArgumentException.class);
}
팩토리 메서드로 추출해 테스트 코드를 짧게 하면서 동시에 네이밍을 통해 내부를 알아볼 필요가 없기 때문에 테스트 진행 상황에 대한 맥락을 유지할 수 있다.
또한 테스트가 서로 결합되지 않는다.
단 예외가 있는데 모든 테스트 또는 거의 모든 테스트에 사용되는 경우 한번만 인스턴스화 할 수도 있다.
예를 들어 데이터베이스와 작동하는 테스트의 경우 한번만 연결을 초기화 하는 것이 합리적이다.