////
Search
Duplicate
🏮

3. 단위 테스트 구조

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로 지정해 구별하자. 구절 사이에 빈 줄을 추가하거나 준비, 실행, 검증 주석을 각각 앞에 둬서 구분하라.
테스트 픽스처 초기화 코드는 생성자에 두지 말고 정적 팩토리 메소드로 재사용하자. 이런 재사용은 테스트 간 결합도를 사앙히 낮게 유지하고 가독성을 향상시킨다.
엄격한 테스트 명명 규칙을 피하라 프로젝트의 이해관계자들에게 테스트 시나리오를 설명하기 편하게 테스트 메소드의 이름을 짓자. 가독성을 위해 밑줄을 사용해 단어를 구분하고 테스트 대상 메소드 이름을 넣지 말라.
매개변수화된 테스트로 유사한 테스트에 필요한 코드의 양을 줄일 수 있다. 단점은 테스트 이름을 조금 더 포괄적으로 만들어야하기 때문에 가독성이 떨어질 수 있다.
단순한 테스트에만 사용하는 것이 좋다.
검증문 라이브러리를 사용하면 쉽게 읽을 수 있도록 검증문에서 단어 순서를 재구성해 가독성을 향상시킬 수 있다.