단위 테스트 구조

log.yunsik·2022년 11월 14일
0

단위 테스트 구조

AAA 패턴

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

  • Arrange - 준비 구절
  • Act - 실행 구절
  • Assert - 검증 구절

예시

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 문은 테스트가 한 번에 너무 많은 것을 검증한다는 표시다.
테스트에 분기가 있어서 얻는 이점은 없고 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에서는 다음과 같이 해결할 수 있다.

  • @BeforeEach
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);
    }

팩토리 메서드로 추출해 테스트 코드를 짧게 하면서 동시에 네이밍을 통해 내부를 알아볼 필요가 없기 때문에 테스트 진행 상황에 대한 맥락을 유지할 수 있다.
또한 테스트가 서로 결합되지 않는다.

단 예외가 있는데 모든 테스트 또는 거의 모든 테스트에 사용되는 경우 한번만 인스턴스화 할 수도 있다.
예를 들어 데이터베이스와 작동하는 테스트의 경우 한번만 연결을 초기화 하는 것이 합리적이다.

참고자료

https://book.interpark.com/product/BookDisplay.do?_method=detail&sc.prdNo=354081442&gclid=CjwKCAiA68ebBhB-EiwALVC-NluNgtNGGTd9IhINWBQ4QNWDbE9I5hxwALdYTDqBsdcmu9l5OYQzwxoCQpIQAvD_BwE=

0개의 댓글