•
3장에서 다루는 내용
◦
단위 테스트 구조
◦
좋은 단위 테스트 네이밍
◦
매개변수화된 테스트 작성
◦
Fleunt Assertions 사용
•
이 장에서는 일반적으로 준비arrange, 실행act, 검증assert 패턴으로 작성된 단위 테스트의 구조를 살펴본다.
•
단위 테스트 명명 기법도 소개한다.
◦
적절하지 않은 명명 사례를 소개하고 왜 좋은 선택이 아닌지 살펴본다.
•
단위 테스트 프로세스를 간소화하는 데 도움이 되는 프레임워크의 몇 가지 기능에 대해 얘기를 나눈다.
1. 단위 테스트를 구성하는 방법
1. AAA 패턴 사용
•
AAA 패턴은 각 테스트 메소드들을 준비, 실행, 검증이라는 세 부분으로 나눌 수 있다.
public class Calculator {
public double sum(double first, double second) {
return first + second;
}
}
Java
복사
public class CalculatorTests {
public void sumOfTwoNumbers() {
// arrange
double first = 10;
double second = 20;
Calculator calculator = new Calculator();
// act
double result = calculator.sum(first, second);
// assert
Assert.equal(30, result);
}
}
Java
복사
•
AAA 패턴은 모든 테스트가 단순하고 균일한 구조를 갖는데 도움이 된다.
◦
준비 구절에서는 SUT과 해당 의존성을 원하는 상태로 만든다.
◦
실행 구절에서는 SUT에서 메서드를 호출하고 준비된 의존성을 전달하며 출력 값을 캡처한다.
◦
검증 구절에서는 결과를 검증한다. 결과는 반환값이나 테스트 대상 시스템과 협력자의 최종 상태, 테스트 대상 시스템이 협력자에 호출한 메서드 등으로 표시될 수 있다.
Given-When-Then 패턴
•
AAA와 유사한 Given-When-Then 패턴에 대해서 들어봤을 것이다. 이 패턴도 테스트를 세 부분으로 나눈다.
◦
Given - 준비 구절에 해당
◦
When - 실행 구절에 해당
◦
Then - 검증 구절에 해당
•
구조적으로 테스트 구성 측면에서는 둘 사이에 차이가 없으나 유일한 차이점은 프로그래머가 아닌 사람에게 이 구조가 더 읽기 쉽다는 것이다.
•
그러므로 Given-When-Then 구조는 비기술자들과 공유해야하는 경우 작성하기에 알맞은 테스트 구조다.
2. 여러 개의 준비, 실행, 검증 구절 피하기
•
준비, 실행, 검증 구절이 여러 개 존재하는 테스트는 적절한 단위 테스트가 아니다.
•
통합 테스트의 경우, 느릴 가능성이 있는(다른 의존성에 공유한다던지) 테스트들은 하나의 테스트 메소드에 묶어 두어도 괜찮다.
◦
이는 통합 테스트에 국한적인 최적화 방법이다.
3. 테스트 내 if 문 피하기
•
테스트에서의 if 문은 안티 패턴이다. 분기가 없는 일련의 간단한 단계여야 한다.
•
if 문은 테스트가 너무 많은 테스트 책임을 가지고 있다는 지표다. 그러므로 반드시 나눠서 처리하자.
4. 각 구절은 사이즈는 어느정도여야 하는가?
•
준비 구절
◦
일반적으로 준비 구절이 세 구절 중에 가장 크다.
◦
준비 구절이 실행, 검증 구절 둘을 합친것보다 더 크다면 같은 테스트 클래스 내 비공개 메소드 또는 별도의 팩토리 클래스로 분리하는 것이 좋다.
▪
이에 도움을 주는 패턴으로 오브젝트 마더와 테스트 데이터 빌더가 있다.
•
실행 구절은 한 줄이 좋다.
◦
실행 구절은 보통 한 줄 코드인 것이 좋다. 실행 구절이 두 줄 이상인 경우 내 코드에 문제가 있을 수 있다.
◦
다음과 같은 예시를 살펴보자.
•
한 줄로 된 실행 구절
public void purchaseSucceedsWhenEnoughInventory() {
// arrange
Store = new Store;
store.addInventory(Product.Shampoo, 10);
Customer customer = new Customer();
// act
bool success = customer.purchase(store, Product.Shampoo, 5);
// assert
Assert.True(success);
Assert.Eqaul(5, store.getInvetory(Product.Shampoo));
}
Java
복사
•
두 줄로 된 실행 구절
public void purchaseSucceedsWhenEnoughInventory() {
// arrange
Store = new Store;
store.addInventory(Product.Shampoo, 10);
Customer customer = new Customer();
// act
bool success = customer.purchase(store, Product.Shampoo, 5);
store.removeInventory(success, Product.Shampoo, 5);
// assert
Assert.True(success);
Assert.Eqaul(5, store.getInvetory(Product.Shampoo));
}
Java
복사
◦
테스트 자체는 구매 프로세스라는 동일한 동작 단위를 테스트하고 있다. 따라서 테스트는 문제가 없다.
◦
여기서 문제가 있는 부분은 Customer 클래스의 API다.
▪
비즈니스 관점에서 구매가 정상적으로 이뤄지면 고객이 제품을 획득하고 매장의 제고가 감소하는 두 가지 결과가 만들어진다.
▪
이 두 메소드는 같은 시간에 일어난다. 즉 이들을 묶어서 처리해야 한다는 것이다.
▪
해결책은 코드 캡슐화를 항상 지키는 것이다. Customer의 메소드 하나로 고객이 매입한 재고를 줄이고 구체적인 코드에 의존하지 않아야 했다.
◦
실행 구절을 한 줄로 하는 지침은 대부분의 비즈니스 로직 코드에 적용되지만 유틸리티나 인프라 코드는 덜 적용된다.
5. 검증 구절에는 검증문이 얼마나 있어야 하는가
•
테스트 당 하나의 검증문을 가지라는 지침은 올바른 지침이 아니다.
•
단일 동작 단위임에도 여러 결과를 반환할 수 있으며 하나의 테스트로 그 모든 결과를 평가하는 것이 좋다.
•
검증 구절이 너무 커지는 경우 제품 코드의 추상화가 누락되었을 수도 있다.
◦
예를 들어 테스트 대상 시스템에서 반환된 객체 내에서 모든 속성을 검증하는 대신 객체 클래스 내에 적절한 동등 멤버를 정의하는 것이 좋다.
◦
그러면 단일 검증문으로 객체를 기대값과 비교할 수 있다.
6. 종료 단계는 어떤가
•
이는 테스트에 의해 작성된 파일을 지우거나 데이터베이스 연결을 해제하고자 사용하고자 종료 구절을 따로 구분하기도 한다.
•
종료는 일반적으로 별도의 메소드로 도출되어 클래스 내 모든 테스트에서 재사용된다. AAA 패턴에서는 이 단계를 포함하지 않는다.
•
단위 테스트는 프로세스 외부에 종속성이 없어야 하므로 처리해야할 사이드 이펙트를 남기지 않기 때문이다. 이는 통합 테스트의 영역이다.
7. 테스트 대상 시스템 구별하기
•
애플리케이션에서 호출하고자 하는 동작에 대한 진입점을 제공하기 때문에 테스트 대상 시스템은 중요하다.
◦
진입점이란 동작을 수행할 하나의 클래스를 의미한다.
•
테스트 대상 시스템과 의존성을 구분하는 것이 중요하다.
•
특히 테스트 대상 시스템이 너무 큰 경우 SUT를 찾기 위해 시간을 많이 사용하는 것은 어리석은 짓이다. 따라서 테스트 대상 시스템의 변수명을 sut로 하라.
public class CalculatorTests {
public void sumOfTwoNumbers() {
double first = 10;
double second = 20;
Calculator sut = new Calculator();
double result = sut.sum(first, second);
Assert.equal(30, result);
}
}
Java
복사
8. 준비, 실행, 검증 주석 제거하기
•
테스트 내에서 특정 부분이 어떤 구절에 속해있는지 구분하는 데 두 가지 방법이 쓰인다. 주석과 줄바꿈이다.
◦
줄바꿈으로 구분하면 대부분의 단위 테스트에서 효과적이며 간결성과 가독성 사이에서 균형을 잡을 수 있다.
▪
테스트가 커진다면 잘 작동하지 않는다. 대규모 테스트에서는 준비 단계에 줄바꿈을 추가해 설정 단계를 구분해야할 수도 있다.
◦
AAA 패턴을 따르고 준비, 검증 구절에 빈 줄을 추가하지 않아도 되는 테스트라면 구절 주석(arrange, act, assert)를 제거하라. 그렇지 않다면 유지하라.
⇒ arrange 구문이 beforeEach에서 처리되어 구문이 존재하지 않는다면 주석을 표시하는 것이 낫겠다.
2. xUnit 테스트 프레임워크 살펴보기
•
저자가 xUnit을 다른 단위 테스트 프레임워크보다 더 선호하는 이유는 더 간결하고 깨끗하기 때문이다.
3. 테스트 간 테스트 픽스쳐 재사용
•
코드의 양을 줄이면서 단순화하기 좋은 방법중 하나는 준비 구절에서 코드를 재사용하는 것이다.
•
테스트 픽스처는 먼저 다음과 같은 두 가지 공통된 의미가 존재한다.
◦
테스트 픽스처는 테스트 실행 대상 객체로 SUT에 전달되는 인수다.
▪
데이터베이스의 데이터나 파일 시스템의 파일일 수도 있다. 이러한 객체는 테스트 실행 전에 고정된 상태로 유지되기 때문에 동일한 결과를 생성한다.
◦
다른 정의는 NUnit 테스트 프레임워크에서 비롯됐다. NUnit에서 [TextFixture]는 테스트가 포함된 클래스를 표시하는 특성이다.
•
테스트 픽스처를 재사용하는 첫 번째 올바르지 않은 방법은 다음과 같이 테스트 생성자에서 픽스처를 초기화하는 것이다.
public class CustomerTests {
private final Store _store;
private final Customer _sut;
public CustomerTests {
_store = new Store();
_store.addInventory(Product.Shampoo, 10);
_sut = new Customer();
}
}
Java
복사
•
테스트 간에 공통된 준비 로직이 존재했기에 초기화 로직을 생성자로 추출했다. 이렇게 했더니 기존에 존재했던 두 개의 테스트에는 더이상 준비구절이 남아있지 않았다.
•
하지만 이런 방법은 두 가지 중요한 단점이 있다.
◦
테스트 간 결합도가 높아진다.
◦
테스트 가독성이 떨어진다.
1. 테스트 간의 높은 결합도는 안티 패턴이다.
•
위의 초기화 로직은 테스트가 결합되어있다. 테스트의 준비 로직인 초기화 함수를 수정한다면 모든 테스트에 영향을 미친다.
•
이는 중요한 지침을 위반한다. 하나의 테스트를 수정하더라도 다른 테스트는 독립적으로 되어있기 때문에 영향을 받아서는 안 된다.
◦
물론 이는 독립적으로 수정되어야 한다는 뜻이다. 단위 테스트의 세 번째 정의였던 실행과는 완전히 동일하지 않다.
•
이 지침을 따르려면 테스트 클래스에 공유 상태를 두지 말아야 한다.
2. 테스트 가독성을 떨어뜨리는 생성자 사용
•
준비 코드를 생성자로 추출했을 때 또 다른 단점은 테스트 가독성을 떨어트린다는 것이다.
•
테스트만 보고서는 하나의 완성된 시나리오를 더 이상 짐작할 수 없게 된다. 초기화 로직까지 봐야 테스트가 무엇을 준비하여 실행한 후 검증하는 지 완전하게 이해된다.
•
준비 로직이 별로 없더라도 테스트 메서드에 두는 것이 좋다.
3. 더 나은 테스트 픽스처 재사용법
•
테스트 픽스처를 재사용할 때 생성자 사용이 최선의 방법은 아니다.
•
저자가 살펴볼 두 번째 방법은 테스트 클래스에 비공개 팩토리 메소드를 두는 것이다.
public class CustomerTests {
private Store CreateStoreWithInventory(Product product, int quantity) {
Store store = new Store();
store.addInventory(product, quantity);
return store;
}
private static Customer createCustomer() {
return new Customer();
}
}
Java
복사
◦
테스트하기 위한 객체를 초기화하는 로직을 추출하여 짧게 하면서도 동시에 테스트 메소드의 시나리오를 유지할 수 있다.
◦
비공개 정적 팩토리 메소드를 충분히 추상화하는 한 테스트가 서로 결합되지 않는다.
•
만약 모든 테스트 메소드가 특정한 인스턴스를 사용한다면 생성자에서 픽스처를 인스턴스화할 수 있다.
◦
데이터베이스와 작동하는 통합 테스트는 이에 종종 해당한다.
4. 단위 테스트 명명법
•
테스트에 표현력이 있는 이름을 붙이는 것이 중요하다. 올바른 명칭은 테스트가 검증하는 내용과 시스템의 동작을 이해하는 데 도움이 된다.
•
가장 유명하면서도 도움이 되지 않는 방법 중 하나가 다음과 같은 관습이다.
◦
${테스트 대상 메소드}_${시나리오}_${결과}
▪
테스트 대상 메소드: 테스트 중인 메소드의 이름
▪
시나리오: 메소드를 테스트하는 조건
▪
결과: 현재 시나리오에서 테스트 대상 메소드에게 기대하는 바
•
동작에 집중하기보다 구현 세부 사항에 집중하기 때문에 도움이 되지 않는다.
•
간단하고 쉬운 영어 구문이 엄격한 명명 구조에 얽매이지 않으며 표현력도 더 뛰어나다.
◦
다음 두 네이밍을 비교해보자.
▪
sumOfTwoNumbers vs sum_twoNumbers_returnsSum
◦
쉬운 영어로 작성한 좌측의 이름이 읽기 쉽다.
단위 테스트 명명 지침
•
표현력있고 가독성 좋은 테스트 이름을 지으려면 다음 지침을 따르자.
◦
엄격한 명명 규칙을 따르지 않는다. 복잡한 동작에 대한 복잡한 설명을 규칙만으로는 작성할 수 없다. 표현의 자유를 허용하자.
◦
문제 도메인에 익숙한 비개발자들에게 시나리오를 설명하는 것처럼 테스트 이름을 짓자.
◦
단어를 밑줄로 구분한다. 그러면 특히나 긴 이름을 가진 메소드들의 가독성이 향상된다.
◦
테스트 이름에 SUT의 메서드 이름을 포함하지 말자. 코드를 테스트하는 것이 아니라 애플리케이션의 동작을 테스트하는 것이다.
•
테스트 클래스의 이름을 지을 때, 런던파는 클래스명Tests를 사용할테지만 우리는 동작을 단위로 생각하기로 했다.
•
그럼 네이밍을 어떻게해야할까? 진입점클래스명Tests와 같이 해두자.
5. 매개변수화된 테스트 리팩토링하기
•
보통 테스트 하나로는 동작 단위를 설명하기에 불충분하다.
◦
이 단위는 일반적으로 여러 구성 요소를 포함하며 각 구성 요소는 자체 테스트로 처리해야한다.
◦
동작이 복잡하다면 이를 구현하기 위해 테스트 클래스에 테스트가 증가할 수 있으며 관리하기 어려워질 수 있다.
•
대부분의 단위 테스트 프레임워크는 매개변수화된 테스트를 사용해 유사한 테스트끼리 묶을 수 있는 기능을 제공한다.
•
가장 빠른 배송일이 오늘로부터 이틀 후가 되도록 작동하는 배송 기능이 있다고 가정하자.
◦
이는 분명히 테스트 메소드 하나로 설명이 가능하지 않다.
◦
지난 배송일을 확인하는 테스트 외에 오늘 날짜, 내일 날짜 그리고 그 이후의 날짜까지 확인하는 테스트도 필요하다.
◦
이들을 각각 테스트로 작성하면 4개의 테스트가 작성될 것이다. 하지만 배송 날짜를 제외한 나머지는 같으므로 매개변수화된 테스트를 이용해 하나로 묶을 수 있다.
•
매개변수화된 테스트를 사용하면 테스트 코드의 양을 크게 줄일 수 있지만 비용이 발생한다. 테스트 메소드가 나타내는 의미를 이해하기가 어려워진 것이다.
◦
이에 대한 절충안으로 긍정적인 테스트 케이스는 고유한 테스트로 도출해두고 가장 중요한 부분을 설명하는 이름을 붙인다.
◦
부정적인 테스트 케이스는 매개변수화된 테스트를 유지한다. 너무 복잡하다면 기존의 테스트를 유지하라.
6. 검증문 라이브러리를 사용한 테스트 가독성 향상
•
JUnit도 자체적으로 Assert를 제공하지만 AssertJ를 쓰는 사람이 많듯 아마 가독성을 위해 사용하는 것 같다.
•
저자는 검증문 라이브러리를 사용하는 주요 이점은 검증문을 재구성해 가독성을 높일 수 있기 때문이다.
◦
검증문 라이브러리들은 일반적으로 메소드의 네이밍이 조금 더 읽기 쉽기 때문이다.
요약
•
모든 단위 테스트는 AAA 패턴을 따라야 한다.
◦
테스트 내 준비, 실행, 검증 구절이 여러개 존재하면 테스트가 여러 동작 단위를 한 번에 검증한다는 증거다. 단위 테스트라면 이들은 분리해야 한다.
•
실행 구절이 한 줄이상이면 테스트 대상 코드에 문제가 있는 것이다.
◦
클라이언트가 항상 이러한 작업을 수행해야 하고 이로 인해 잠재적인 모순으로 이어질 수 있다. 이러한 모순을 불변 위반이라고 한다.
◦
잠재적인 불변 위반으로부터 코드를 보호하는 것을 캡슐화라고 한다.
•
테스트 내에서 SUT의 이름을 sut로 지정해 구별하자. 구절 사이에 빈 줄을 추가하거나 준비, 실행, 검증 주석을 각각 앞에 둬서 구분하라.
•
테스트 픽스처 초기화 코드는 생성자에 두지 말고 정적 팩토리 메소드로 재사용하자. 이런 재사용은 테스트 간 결합도를 사앙히 낮게 유지하고 가독성을 향상시킨다.
•
엄격한 테스트 명명 규칙을 피하라 프로젝트의 이해관계자들에게 테스트 시나리오를 설명하기 편하게 테스트 메소드의 이름을 짓자. 가독성을 위해 밑줄을 사용해 단어를 구분하고 테스트 대상 메소드 이름을 넣지 말라.
•
매개변수화된 테스트로 유사한 테스트에 필요한 코드의 양을 줄일 수 있다. 단점은 테스트 이름을 조금 더 포괄적으로 만들어야하기 때문에 가독성이 떨어질 수 있다.
◦
단순한 테스트에만 사용하는 것이 좋다.
•
검증문 라이브러리를 사용하면 쉽게 읽을 수 있도록 검증문에서 단어 순서를 재구성해 가독성을 향상시킬 수 있다.