본문 바로가기
✅ 테스트

단위 테스트 구조 (AAA 패턴, 테스트 픽스터, 명명법)

by kukim 2022. 1. 4.

이 글은 책 Unit Testing(단위 테스트) 3장과 하단 Reference 참고했습니다. 잘못된 내용이 있다면 편하게 말씀해주세요 🙏🏻

  • 단위 테스트 구조
  • 테스트 픽스처 재사용
  • 테스트 명명법
  • Parameterized Test (매개변수화 테스트)
  • Assert Library (AssertJ)

1. 단위 테스트 구성 방법

  • 단위 테스트 코드에 대한 구성 (준비, 실행, 검증) 패턴을 공부하고 피해야 할 함정, 읽기 쉬운 방법을 알아본다

1.1 AAA 패턴 사용

아래는 Calculator 클래스의 sum 함수를 테스트하는 코드 예이다.

public class Calculator {

    public double sum(double first, double second) {
        return first + second;
    }
}

public class CalculatorTests {
    @Test
    void sumOfTwoNumbers() {
        // Arrange(준비)
        double first = 10.0;
        double second = 20.0;
        Calculator calculator = new Calculator();

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

        // Assert(검증)
        assertEquals(30.0, result);
    }
}

AAA 패턴은 테스트 코드를 세 부분으로 나눌 수 있다.

 

AAA 패턴

  • 스위트 내 모든 테스트가 단순하고 균일한 구조를 갖는 데 도움이 된다. → 일관성
  • 이는 곧 유지 보수 비용 줄어든다.
  1. Arrange(준비 구절) : 테스트 대상 시스템(SUT, System Under Test)과 해당 의존성을 원하는 상태로 만든다. (테스트 대상 메서드, 클래스에 필요한 파라미터나 인스턴스 생성을 한다.)
  2. Act(실행 구절) : SUT에서 메서드를 호출하고 준비된 의존성을 전달하며 결과, 출력 값이 있다면 저장한다. (테스트 대상 메서드, 인스턴스의 리턴 결과나 출력이 있다면 변수에 저장한다.)
  3. Assert(검증 구정) : 결과를 검증한다. 결과는 반환 값, SUT와 협력자의 최종 상태, SUT가 협력자에 호출한 메서드 등으로 표시될 수 있다. (리턴, 출력 값이나 인스턴스 안이나 밖의 연결된 다른 인스턴스의 상태나 메서드 결과를 체크한다)

Given-When-Then 패턴과 AAA 패턴

두 패턴은 구성 측면에서 동일하다. 차이점은 Given-When-Then 구조가 사람이 더 읽기 쉽다.

  • Given - Arrange(준비)
  • When - Act(실행)
  • Then - Assert(검증)

AAA 패턴 적용의 시작은?

  • 테스트 코드 작성 시 보통 Arragne(준비)부터 시작하는 것이 자연스럽다.
  • TDD를 실천할 때, 기능 개발 전 실패할 테스트를 만들 때는 아직 기능이 어떻게 동작할지 충분히 할지 못하기 때문에, 기대하는 동작으로 윤곽을 잡은 다음 개발한다. → ‘검증(Act) 구절’부터작성하기도 한다.
  • 보통, 테스트 전 제품 코드(기능 구현)를 먼저 하기 때문에 준비 구절부터 하는 것이 좋다.

1.2 테스트 내 if 문 피하기

  • 테스트 내 if문은 안티 패턴 → 한 번에 너무 많은 검증한다는 표시이니 나눠서 할 것

1.3 각 구절을 얼마의 크기여야 하는가?

Arrange 큰 경우 -> 팩토리 클래스(본문 목차 3. 테스트 간 테스트 픽스처 재사용 참고)

일반적으로 준비 구절이 가장 길다. 만약 준비 구절이 실행 + 검증보다 크다면 같은 Arrange를 테스트 클래스 내 private 메서드 or 별도의 팩토리 클래스를 만드는 것이 좋다. 준비 구절 코드 재사용에 도움 되는 두 패턴으로는 Object Mother(오브젝트 마더)와 테스트 데이터 빌더(Test Data Builder)가 있다.

 

실행 구절이 한 줄 이상인 경우 경계

  • 실행이 두 줄 이상이라면 테스트 자체는 문제가 되지 않지만 기능 구현 코드 자체 문제일 수 있다 (메서드 간 의존적, 캡슐화되어있지 않음)
  • 하지만 유틸이나 인프라 코드는 덜 적용된다.
  • 비즈니스 코드 테스트가 두 줄 이상이라면 캡슐화 위반을 체크해봐라

1.4 하나의 테스트에 적당한 Assert 수는?

단위 테스트의 Assert(검증)은 가능하면 하나의 단위가 좋다.

하지만 이때 착각은 한 줄의 assertThat() 이 아니라 단위 테스트 단위는 동작의 단위다. 코드 단위가 아니다

단일 동작 단위는 여러 결과를 낼 수 있으며, 하나의 테스트로 그 모든 결과를 평가하는 것이 좋다.

  • 하나 메서드에 여러 검증을 한다면 → 유닛 테스트가 아니라 통합 테스트이다 (p83)

1.5 종료 단계

테스트에 의해 만들어진 파일을 지우거나 DB 연결을 끊는 종료 단계가 있다. 보통 AAA 패턴에서는 이 단계를 포함하지 않고 통합 테스트의 영역에서 본다.

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

  • 테스트의 Act(실행) 부분의 SUT 한 번에 찾기 힘들 때는 SUT의 변수명을 sut로 쓰자
public class Calculator {

    public double sum(double first, double second) {
        return first + second;
    }
}

public class CalculatorTests {
    @Test
    void sumOfTwoNumbers() {
        // Arrange(준비)
        double first = 10.0;
        double second = 20.0;
        Calculator sut = new Calculator(); // SUT를 sut 변수명으로 사용

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

        // Assert(검증)
        assertEquals(30.0, result);
    }
}

1.7 준비, 실행, 검증을 빈칸으로 구분하기 (주석 X)

AAA 패턴을 따르고 있다면 주석 제거하고, 규모가 커 구분하기 힘들거나 모두가 정해진 패턴이 아니라면 주석을 유지하라

public class Calculator {

    public double sum(double first, double second) {
        return first + second;
    }
}

public class CalculatorTests {
    @Test
    void sumOfTwoNumbers() {
        double first = 10.0;
        double second = 20.0;
        Calculator calculator = new Calculator();

        double result = calculator.sum(first, second);

        assertEquals(30.0, result);
    }
}

2. xUnit 프레임워크

  • 자바의 경우 JUnit5를 학습하자.

3. 테스트 간 테스트 픽스처(test fixture) 재사용

테스트 픽스처는 Act(실행) 단계 전 Arrange(준비)의 모든 것이라고 할 수 있다. e.g. 테스트 메서드에 들어갈 변수 설정, 인스턴스 생성 등

테스트 간 테스트 픽스처가 반복되면 재사용할 수 있다.

  • 테스트 픽스처 재사용은 테스트를 줄이고 단순화하기 좋은 방법이다.
  • 생성자를 통한 재사용은 테스트 간 결합도가 높아져 안티 패턴이다. 따라서 테스트는 서로 격리돼 실행한다는 것으로 잡아야 한다.
  • private factory method(비공개 팩토리 메서드)를 사용해 초기화하는 방법은 도움이 된다.

아래 글을 통해 더 디테일하게 살펴볼 수 있다.

 

테스트 픽스처 올바르게 사용하기

xUnit에서는 테스트 대상 시스템 (System Under Test, 이하 SUT) 를 실행하기 위해 해줘야 하는 모든 것을 테스트 픽스처라고 부른다. 처음 테스트 코드를 배우게 되면 이 테스트 픽스처 부분에 대해서

jojoldu.tistory.com

4. 단위 테스트 명명법

  • 엄격한 명명 정책을 따르면 더 복잡해질 수 있다. 표현의 자유를 허용하자.
  • 도메인에 익숙한 비개발자들도 쉽게 이해할 수 있는 이름으로 짓자.
  • _(underscore)로 구분하자. (긴 이름 가독성 향상)
  • 테스트명에 테스트 대상 메서드(SUT)를 포함하지 말라
    • 하지만, 유틸리티 코드 작업할 때는 비즈니스 로직없이 코드 동작이 단순한 보조 기능에 벗어나지 않으므로 SUT 메서드 이름 사용해도 좋다 (단순한 메서드 체크?)
IsDeliveryValid_invalidDate_ReturnsFasle()
-> Delivery_with_invalid_date_should_be_considered_invalid()
// IsDeveliveryValid는 메서드이름이었기 때문에 테스트명에 포함 X
// 프로그래머가 아닌 사람들에게도 납득이 가능, 프로그래머도 더 이해 쉬움 

-> Delivery_with_past_date_should_be_considered_invalid()
// 음 뭔가 장황하고, considered 제거해도 괜찮아 보이는걸?

-> Delivery_with_past_date_should_be_invalid()
// should be 는 안티패턴이다. 테스트는 동작 단위 단순하고 원자적이니 is로 하자

-> Delivery_with_past_date_is_invalid()
// 관사 a 추가하면

-> Delivery_with_a_past_date_is_invalid()
// Umm . . . good?

5. 유사하고 반복적인 테스트 리팩터링 : Parameterized Test ! 

// 예를 들어 배송 날짜만 차이가 있는 테스트를 여러개 반복한다고 하자.

Delivery_for_today_is_invalid()
Delivery_for_tommorow_is_invalid()
...
// 여러개 테스트를 만들어야할까?

메서드의 기능이 많거나, 예외 처리할 것이 많다면 테스트 수가 많아진다. → 매개변수화된 테스트(parameterized test)를 사용하여 유사한 테스트를 묶어 사용하자

자바의 경우 @ParameterizedTest을 사용하여 여러 매개변수를 하나의 테스트에서 사용할 수 있다.

 

예를 들어 아래는 처음 살펴본 덧셈 테스트이다. 유사한 테스트 3개가 있다. parameterized test 적용을 통해 테스트 코드를 리팩토링 할 수 있다. 

 

사용 전

public class Calculator {

    public double sum(double first, double second) {
        return first + second;
    }
}


public class CalculatorTests {
    @Test
     void sum_Of_Two_Postive_Numbers() {
        double first = 10.0;
        double second = 20.0;
        Calculator sut = new Calculator();

        double result = sut.sum(first, second);

        assertEquals(30.0, result);
    }

    @Test
    void sum_Of_Two_Negative_Numbers() {
        double first = -10.0;
        double second = -20.0;
        Calculator sut = new Calculator();

        double result = sut.sum(first, second);

        assertEquals(-30.0, result);
    }


    @Test
    void sum_Of_Two_Zero() {
        double first = 0.0;
        double second = 0.0;
        Calculator sut = new Calculator();

        double result = sut.sum(first, second);

        assertEquals(0.0, result);
    }
}

사용 후

이전 코드보다 테스트 코드 반복가 깔끔하다. 기존 JUnit의 @Test 대신 @ParameterizedTest를 사용하였고 파라미터 매개변수 넘겨주는 것은 어려 옵션이 있는데 @MethodSource("parametersProvider") 어노테이션을 통해 parameterProvider() 메서드의 매개변 수들을 차례대로 테스트에 넘어가고 그 순서의 인자는 double first, ... double answer 으로 넘어가 테스트에서 동일하게 사용할 수 있다.

public class Calculator {
    public double sum(double first, double second) {
        return first + second;
    }
}


public class CalculatorTests {
    @ParameterizedTest
    @MethodSource("parametersProvider")
    void sum_Of_Two_Numbers(double first, double second, double answer) {
        Calculator sut = new Calculator();

        double result = sut.sum(first, second);

        assertEquals(answer, result);

    }

    static Stream<Arguments> parametersProvider() {
        return Stream.of(
                Arguments.arguments(10.0, 20.0, 30.0),
                Arguments.arguments(-10.0, -20.0, -30.0),
                Arguments.arguments(0.0, 0.0, 0.0)
        );
    }

}

 

6. assert Library 검증문 라이브러리 (Java : AssertJ)

자바 JUnit5 경우 자체 assert 하는 메서드가 있는데 가독성이 떨어지거나 사용법이 복잡하기 때문에 AssertJ 를 사용하면 가독성과 편의성이 높아진다.

public class CalculatorTests {
    @ParameterizedTest
    @MethodSource("parametersProvider")
    void sum_Of_Two_Numbers(double first, double second, double answer) {
        Calculator sut = new Calculator();

        double result = sut.sum(first, second);

        assertThat(result).isEqualTo(answer); // assertJ
    }
}

요약

  • 단위 테스트 작성 시 AAA 패턴을 따르자
  • 실행 구절이 한 줄 이상이면 캡슐화되지 않았을 가능성이 있다. 제품 코드를 확인해보자
  • SUT의 이름을 sut로 지정해 SUT를 테스트에서 구별하자.
  • AAA 패턴 사이에 주석보단 공백으로 구분하자
  • 테스트 픽스처 초기화 코드는 생성자보다는 팩토리 메서드 도입을 이용하자. 반복되는 준비 과정은 재사용을 통해
  • 엄격한 테스트 명명보다는 비개발자도 이해할 수 있도록 쉽게 쓰자.
  • 파라미터만 다른 테스트가 있다면 매개변수화된 테스트를 이용하자
  • 검증문 라이브러리를 사용하자 (assertJ)

⛓ Reference

JUnit5 Parameterized Guide  - baeldung

JUnit5 사용법 - Parameterized Tests

AssertJ가 JUnit의 assertThat 보다 편리한 이유 - 백기선

테스트 픽스처 올바르게 사용하기 - 향로

댓글