1. 단위 테스트란 무엇인가?
- 단위 테스트의 정의에는 많은 미묘한 차이들이 있습니다.
- 해석에 차이에 따라 고전파와 런던파로 두 가지로 나뉩니다.
- 고전파: 단위 테스트를 서로 격리하는 것
- 런던파: 테스트 대상 시스템을 협력자로부터 격리하는 것
- 자세한 내용은 아래에서 설명합니다.
2. 단위 테스트 정의
- 가장 먼저 단위 테스트의 정의를 살펴봅시다.
단위 테스트란?
- 작은 코드 조각을 검증하고
- 빠르게 수행하고
- 격리된 방식으로 처리하는 자동화된 테스트다
- 먼저 첫 번째 속성은 논란의 여지가 없습니다.
- 두 번째 속성은 정확히 빠르다는 것은 주관적인 척도이므로 논쟁이 있을 수 있지만 그리 중요하지 않습니다.
- 테스트 스위트의 실행 시간이 충분하다면 테스트가 충분히 빠르다는 의미입니다.
- 대중의 의견이 갈리는 것은 세 번째 속성이다.
- 고전파와 런던파의 견해가 이 속성에서 갈린다
3. 고전파와 런던파
- 단위 테스트의 세 번째 속성에서 격리가 정확히 무엇인지에 대한 의견 차이로 고전파와 런던파로 나누어집니다.
3.1 런던파의 단위 테스트 정의
- 런던파의 단위 테스트는
- 단일 클래스 내의 메서드를 검증하며
- 빠르게 수행하고
- 테스트 대상을 협력자로부터 격리하는 자동화된 테스트
- 런던파의 격리는 테스트 대상 시스템(SUT)을 협력자로부터 격리하는 것을 의미합니다.
3.2 고전파의 단위 테스트 정의
- 고전파의 단위 테스트는
- 단일 동작 단위를 검증하며
- 빠르게 수행하고
- 다른 테스트와 별도로(격리하여) 처리하는 자동화된 테스트
- 고전파의 격리는 단위 테스트를 서로 격리하는 것을 의미합니다.
4. 격리 문제에 대한 런던파 접근
- 런던파가 생각하는 격리란 아래와 같습니다.
- 테스트 대상 시스템(SUT)을 협력자에게서 격리하는 것
- 즉 하나의 클래스가 다른 클래스 또는 여러 클래스에 의존하면 이 모든 의존성을 테스트 대역으로 대체해야 합니다.
- 불변 의존성은 제외한 모든 의존성을 대체합니다.
- 그래서 런던파를 목 추종자라고 부릅니다.
- 테스트 대역은 Test-Double.md 참고해주세요.
SUT
SUT(System Under Test)는 테스트 대상 시스템을 말합니다.
4.1 런던파 접근의 장점
- 테스트가 실패하면 코드베이스의 어느 부분이 고장 났는지 확실히 알 수 있습니다.
- 테스트가 실패하면 테스트 대상 시스템이 고장난 것입니다.
- 테스트 대상 시스템 클래스의 모든 의존성은 테스트 대역으로 대체됐기 때문에 의심할 여지가 없습니다.
- 객체 그래프를 분할할 수 있어 서로 연결된 클래스의 그래프가 커져도 테스트하기 쉽습니다.
- 실제 협력자 대신해 목을 사용하면 클래스를 쉽게 테스트할 수 있습니다.
- 특히 SUT 클래스에 의존성이 있고 이 의존성에 다시 각각의 의존성이 있는 복잡한 객체 그래프가 있을 때 쉽게 테스트할 수 있습니다.
- 테스트 대역을 쓰면 클래스의 직접적인 의존성을 대체해 객체 그래프를 나눌 수 있습니다.
- 단위 테스트에서 준비해야 할 작업량을 크게 줄일 수 있습니다.
- 고전파의 단위 테스트는 SUT를 설정하려면 전체 공유 의존성을 제외한 전체 객체 그래프를 다시 생성해야 합니다.
- 고전파의 설정이 많은 작업량을 가지고 있어 위는 사실이지만 복잡한 객 체 클래스를 설정해 테스트할 방법을 찾는 대신 근본적으로 복잡한 클래스 그래프를 갖지 않는 데 집중해야 합니다.
- 대게 클래스 그래프가 커진 것은 코드 설계의 문제입니다.
- 간단한 테스트 스위트 구조
- 프로젝트 전반적으로 한 번에 한 클래스만 테스트하라는 지침을 도입하면 전체 단위 테스트 스위트를 간단한 구조로 할 수 있습니다.
- 더 이상 코드베이스를 테스트하는 방법을 고민할 필요가 없습니다.
- 클래스가 있다면 클래스에 해당하는 단위 테스트 클래스를 생성하면 됩니다.
객체 그래프
객체 그래프는 같은 문제를 해결하는 클래스의 통신망을 말합니다.
4.2 런던파 접근의 단점
- 과도한 명세 문제
- 런던 스타일이 고전 스타일 테스트보다 테스트가 구현에 더 자주 결합되는 편입니다.
- 테스트는 단일 동작 단위를 검증해야 한다
- 이보다 작은 단위로 검증하게 되면 이 테스트가 무엇을 검증하는지 정확히 이해하기가 어려워 진다
- 단일 동작 단위를 더 작은 단위로 검증하는 것의 차이는 비유하자면
우리집 강아지를 부르면, 바로 나에게 온다와우리집 강아지를 부르면 먼저 왼쪽 앞다리를 움직이고, 이어서 오른쪽 앞다리를 움직이고, 머리를 돌리고, 꼬리를 흔들기 시작한다의 차이와 같습니다. - 실제 동작(개가 주인에게 오는 것) 대신 개별 클래스(다리, 머리, 꼬리)를 목표로 테스트하면 실제 동작을 테스트 하는 건지 이해하기 힘듭니다.
- 개별 클래스를 목표로 테스트 하면 강아지가 나에게 오고 있는가? 아니면 도망을 가고 있는가? 알 수 없는 테스트가 됩니다.
테스트는 단일 동작 단위를 검증해야 한다
테스트는 코드의 단위를 검증해서는 안 된다. 오히려 동작의 단위, 즉 문제 영역에 의미가 있는 것, 이상적으로 비즈니스 담당자가 유용하다고 인식할 수 있는 것을 검증해야한다. 동작 단위를 구현하는 데 클래스가 얼마나 필요한지는 상관없다. 단위는 여러 클래스에 걸쳐 있거나 한 클래스에만 있을 수 있고, 심지어 아주 작은 메서드가 될 수 있다
4.3 런던파 테스트 예시
- 온라인 상점을 운영한다고 가정하고 상점에 재고가 충분하면 구매는 성공으로, 구매 수량만큼 제품 수량이 줄어듭니다.
- 제품이 충분하지 않으면 구매는 성공하지 못하며 상점에는 아무 일도 일어나지 않습니다.
AAA Pattern
AAA Pattern은 The-Anatomy-Of-A-Unit-Test.md를 참고해주세요.
AAA Pattern 런던파 테스트 코드
@Test
void purchase_succeeds_when_enough_inventory_mock() {
// Arrange
Store storeMock = mock(Store.class);
given(storeMock.hasEnoughInventory(Product.Shampoo, 5))
.willReturn(true);
Customer customer = new Customer();
// Act
boolean success = customer.purchase(storeMock, Product.Shampoo, 5);
// Assert
assertThat(success).isTrue();
verify(storeMock, times(1)).hasEnoughInventory(Product.Shampoo, 5);
}
- 준비 단계
- 테스트는 Store의 실제 인스턴스를 생성하지 않고 Mock 객체를 대신 사용합니다.
- 샴푸 재고를 추가해 Stroe의 상태를 수정하는 대신 hasEnoughInventory() 메서드를 호출에 어떻게 응답하는지 목에 직접 정의합니다.
- 실행 단계
- Customer의 purchase 메서드를 호출합니다.
- 여기서 Customer가 SUT입니다.
- 검증 단계
- Customer의 purchase 호출 결과를 검증합니다.
- 추가적으로 지금은 Customer와 Stroe 간의 상호 작용을 검사합니다.
- 즉 Customer가 Stroe를 올바르게 호출했는지 검증합니다.
5. 격리 문제에 대한 고전파 접근
- 고전파가 생각하는 격리란 단위 테스트를 서로 격리하는 것입니다.
- 단위 테스트를 서로 격리한다는 의미는 단위 테스트가 서로 병렬로 실행될 수 있음을 의미합니다.
- 여러 클래스가 모두 메모리에 상주하고 공유 상태에 도달하지 않는 한 여러 클래스를 한 번에 테스트해도 괜찮습니다.
5.1 공유 상태
- 여러 테스트가 공유 의존성을 가지고 있는 상태를 말합니다.
- 데이터베이스, 파일 시스템 등 프로세스 외부 의존성이 공유 상태의 대표적인 예입니다.
- 예를 들어 어떤 테스트가 준비 단계에서 데이터베이스에 고객을 생성할 수 있고, 이 테스트가 실행되기 전에 다른 테스트의 준비 단계에서 고객을 삭제할 수 있습니다.
- 이 두가지 테스트를 병렬로 실행하면 첫 번째 테스트가 실패하는데 이는 제품 코드의 문제가 아니라 두 번째 테스트의 간섭 때문입니다.
5.2 공유 의존성
- 공유 의존성은 테스트간에 공유되고 서로의 결과에 영향을 미칠 수 있는 수단을 제공하는 의존성입니다.
- 공유 의존성은 프로세스 내부 또는 외부에 존재 할 수 있습니다.
5.3 프로세스 내부 공유 의존성
- 프로세스 내부 공유 의존성의 전형적인 예는 정적 가변 필드와 싱글턴이 있습니다.
- 필드의 변경 사항을 동일한 프로세스 내에서 실행되는 모든 단위 테스트에서 볼 수 있습니다.
- 공유 의존성이 프로세스 내부에 있으면 각 테스트에서 별도의 인스턴스를 공급할 수 있으므로 테스트 간에 공유할 필요가 없습니다.