이 글은 책 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 패턴
- 스위트 내 모든 테스트가 단순하고 균일한 구조를 갖는 데 도움이 된다. → 일관성
- 이는 곧 유지 보수 비용 줄어든다.
- Arrange(준비 구절) : 테스트 대상 시스템(SUT, System Under Test)과 해당 의존성을 원하는 상태로 만든다. (테스트 대상 메서드, 클래스에 필요한 파라미터나 인스턴스 생성을 한다.)
- Act(실행 구절) : SUT에서 메서드를 호출하고 준비된 의존성을 전달하며 결과, 출력 값이 있다면 저장한다. (테스트 대상 메서드, 인스턴스의 리턴 결과나 출력이 있다면 변수에 저장한다.)
- 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(비공개 팩토리 메서드)를 사용해 초기화하는 방법은 도움이 된다.
아래 글을 통해 더 디테일하게 살펴볼 수 있다.
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
'✅ 테스트' 카테고리의 다른 글
단위 테스트란 무엇일까? 런던파와 고전파의 차이점 🆚 (2) | 2022.03.19 |
---|---|
IntelliJ의 Code&Live Templates 활용하여 생산성 높이기! 테스트코드 작성시간 줄이고 아직 구현하지 않은 메서드 예외로 확인하기 (0) | 2022.03.10 |
private 메서드도 테스트를 해야 할까? (private 메서드 테스트 하고 싶을 때...) ✅ 👃 (7) | 2022.02.16 |
Design by Contract(계약에 의한 설계)와 테스트 코드 그리고 예제📝 (2) | 2022.02.07 |
단위 테스트의 목표와 책 단위 테스트 소개 (0) | 2022.01.04 |
댓글