본문으로 건너뛰기

2. 단위 테스트란 무엇인가

2장에서 다루는 내용
  • 단위 테스트란?
  • 공유 의존성, 비공개 의존성, 휘발성 의존성 간의 차이점
  • 단위 테스트의 두 분파: 고전파와 런던파
  • 단위 테스트, 통합 테스트, 엔드 투 엔드 테스트의 차이점

단위 테스트의 정의에는 많은 뉘앙스가 있다. 이런 뉘앙스는 생각보다 훨씬 중요하다. 해석의 차이가 생겼고, 단위 테스트에 접근하는 방법이 두 가지 뚜렸한 견해로 나뉘었다.

이러한 두 가지 견해는 각각 고전파(Classical school)와 런던파(London school)로 알려져 있다. 고전파는 모든 사람이 단위 테스트와 테스트 주도 개발에 원론적으로 접근하는 방식이기 때문에 '고전' 이라고 한다. 런던파는 런던의 프로그래밍 커뮤니티에서 시작됐다.

2.1 '단위 테스트'의 정의

단위 테스트에는 가장 중요한 세 가지 속성이 있다.

  • 작은 코드 조각을 검증하고,
  • 빠르게 수행하고
  • 격리된 방식으로 처리하는 자동화된 테스트다.

처음 두 속성은 논란의 여지가 없다. 대중의 의견이 크게 다른 것은 세 번째 속성이다. 격리 문제는 단위 테스트의 고전파와 런던파를 구분할 수 있게 해주는 근원적 차이에 속한다.

단위 테스트의 고전파와 런던파

고전적 접근법은 '디트로이트'라고도 하며, 때로는 단위 테스트에 대한 고전주의적 접근법이라고도 한다.

런던 스타일은 때때로 '목 추종자'로 표현된다. 목 추종자라는 용어가 널리 퍼져 있지만, 런던 스타일을 따르는 사람들은 보통 그렇게 부르는 것을 좋아하지 않는다.

2.1.1 격리 문제에 대한 런던파의 접근

코드 조각을 격리된 방식으로 검증한다는 것은 무엇을 의미하는가? 런던파에서는 테스트 대상 시스템을 협력자에게서 격리하는 것을 일컫는다. 즉, 하나의 클래스가 다른 클래스 또는 여러 클래스에 의존하면 이 모든 의존성을 테스트 대역으로 대체해야 한다. 이런 식으로 동작을 외부 영향과 분리해서 테스트 대상 클래스에만 집중할 수 있다.

의존성이 포함된 테스트 대상 시스템을 검증하는 단위 테스트는 해당 의존성과 별개로 수행할 수 있다.

이 방법의 한 가지 이점은 테스트가 실패하면 코드베이스의 어느 부분이 고장 났는지 확실히 알 수 있다는 것이다. 즉, 확실히 테스트 대상 시스템이 고장 난 것이다. 클래스의 모든 의존성은 테스트 대역으로 대체됐기 때문에 의심할 여지가 없다.

또 다른 이점은 객체 그래프를 분할할 수 있는 것이다. 모든 클래스가 각각 직접적인 의존성을 갖고 있으며 또 그 의존성이 또 다른 의존성을 갖고 있는 식으로, 그래프가 상당히 복잡해질 수 있다. 클래스는 심지어 순환 의존성이 있을 수도 있는데, 이는 의존성 사슬이 결국 시작된 위치로 돌아오는 것이다.

의존성을 가진 코드베이스를 테스트하는 것은 테스트 대역 없이는 어렵다. 유일하게 남은 선택은 전체 객체 그래프를 다시 만드는 것이다. 하지만 클래스의 수가 너무 많으면 어려운 작업일 수 있다.

테스트 대역을 사용하면 객체 그래프를 다시 만들지 않아도 된다. 또한 클래스의 직접적인 의존성을 대체할 수 있고, 더 나아가 의존성의 의존성을 다룰 필요도 없다. 그래프를 효과적으로 분해해 단위 테스트에서 준비를 크게 줄일 수 있다.

또한 단위 테스트 격리에는 작지만 유익한 부가적인 이점이 더 있다. 프로젝트 전반적으로 한 번에 한 클래스만 테스트하라는 지침을 도입하면 전체 단위 테스트 스위트를 간단한 구조로 할 수 있다. 더 이상 코드베이스를 테스트하는 방법을 고민할 필요가 없다.

몇 가지 예를 살펴보자. 고전적인 스타일이 대부분의 사람들에게 더 익숙하기 때문에 먼저 고전적인 스타일로 작성된 샘플 테스트를 살펴본 후 런던 방식을 사용해 다시 작성해 볼 것이다.

온라인 상점을 운영한다고 가정하자. 샘플 애플리케이션에는 고객에 제품을 구매할 수 있다는 간단한 유스케이스가 하나 있다. 상점에 재고가 충분하면 구매는 성공으로 간주되고, 구매 수량만큼 상점의 제품 수량이 줄어든다. 제품이 충분하지 않으면 구매는 성공하지 못하며 상점에 아무 일도 일어나지 않는다.

예제 2.1에는 상점에 재고가 충분히 있을 때만 구매가 성공하는지 검증하는 두 가지 테스트가 있다.

예제 2.1 고전적인 스타일로 작성된 테스트
// [Fact]
public void Purchase_succeeds_when_enough_inventory()
{
// 준비
var store = new Store();
store.AddInventory(Product.Shampoo, 10);
var customer = new Customer();

// 실행
bool success = customer.Purchase(store, Product.Shampoo, 5);

// 검증
Assert.True(success);
Assert.Equal(5, store.GetInventory(Product.Shampoo)); // 상점 제품 다섯 개 감소
}

// [Fact]
public void Purchase_fails_when_not_enough_inventory()
{
// 준비
var store = new Store();
store.AddInventory(Product.Shampoo, 10);
var customer = new Customer();

// 실행
bool success = customer.Purchase(store, Product.Shampoo, 15);

// 검증
Assert.True(success);
Assert.Equal(10, store.GetInventory(Product.Shampoo)); // 상점 제품 수량 변화 없음
}

public enum Product
{
Shampoo,
Book
}

준비 단계에서는 테스트 대상 시스템(SUT, system under test)과 하나의 협력자를 준비한다. 이 경우 고객이 SUT에, 상점이 협력자에 해당한다. 다음 두 가지 이유로 협력자가 필요하다.

  • 테스트 대상 메서드를 컴파일 하려면 customer.Purchase()가 Store 인스턴스를 인수로 필요로 하기 때문에
  • 검증 단계에서 customer.Purchase()의 결과 중 하나로 상점 제품 수량이 감소할 가능성이 있기 때문에

이 코드는 단위 테스트의 고전 스타일 예로, 테스트는 협력자를 대체하지않고 운영용 인스턴스를 사용한다. 고전적인 방식의 자연스로운 결과로, 이제 Customer만이 아니라 Customer와 Store 둘 다 효과적으로 검증한다. 그러나 Customer가 올바르게 작동하더라도 Customer에 영향을 미치는 Store 내부에 버그가 있으면 단위 테스트에 실패할 수 있다. 테스트에서 두 클래스는 서로 격리돼 있지 않다.

이제 런던 스타일로 예제를 수정해보자. 동일한 테스트에서 Store 인스턴스는 테스트 대역, 구체적으로 목으로 교체해본다.

사람들이 흔히 테스트 대역과 목을 동의어로 사용하지만, 기술적으로는 그렇지 않다. (목이 테스트 대역의 부분집합이다.)

  • 테스트 대역은 실행과 관련 없이 모든 종류의 가짜 의존성을 설명하는 포괄적인 용어다.
  • 목은 그러한 의존성의 한 종류일 뿐이다.

다음 예제에서는 Customer가 협력자인 Store에서 격리된 후 어떻게 테스트가 수행되는지 보여준다.

예제 2.2 런던 스타일로 작성된 단위 테스트
// [Fact]
public void Purchase_succeeds_when_enough_inventory()
{
// 준비
var storeMock = new Mock<IStore>();
storeMock
.Setup(x => x.HasEnoughInventory(Product.Shampoo, 5))
.Returns(true);
var customer = new Customer();

// 실행
bool success = customer.Purchase(
storeMock.Object, Product.Shampoo, 5
);

// 검증
Assert.True(success);
Assert.Verify(
x => x.RemoveInventory(Product.Shampoo, 5),
Times.Once
);
}

// [Fact]
public void Purchase_fails_when_not_enough_inventory()
{
// 준비
var storeMock = new Mock<IStore>();
storeMock
.Setup(x => x.HasEnoughInventory(Product.Shampoo, 5))
.Returns(false);
var customer = new Customer();

// 실행
bool success = customer.Purchase(
storeMock.Object, Product.Shampoo, 5
);

// 검증
Assert.False(success);
Assert.Verify(
x => x.RemoveInventory(Product.Shampoo, 5),
Times.Never
); // 상점 제품 수량 변화 없음
}

public enum Product
{
Shampoo,
Book
}

고전 스타일로 작성된 테스트와 얼마나 다른지 살펴보자. 준비 단계에서 테스트는 Store의 실제 인스턴스를 생성하지 않고 Moq의 내장 클래스인 Mock<T>를 사용해 대체한다.

검증 단계도 바뀌었고 중요한 차이점이 여기에 있다. 여전히 이전과 같이 customer.Purchase 호출 결과를 확인하지만, 고객이 상점에서 올바르게 했는지 확인하는 방법이 다르다. 이전에는 상점 상태를 검증했다. 지금은 Customer와 Store 간의 상호 작용을 검사한다. 즉, 고객이 상점에서 호출을 올바르게 했는지 확인한다. 고객이 상점으로 호출해야 하는 메서드뿐만 아니라 호출 횟수까지 검증할 수 있다. 고객은 구매가 성공하면 이 메서드를 한 번만 호출해야 하고, 구매가 실패하면 절대로 호출하면 안 된다.

2.1.2. 격리 문제에 대한 고전파의 접근

다시 말하면, 런던 스타일은 테스트 대역으로 테스트 대상 코드 조각을 분리해서 격리 요구 사항에 다가간다. 흥미롭게도 이 관점은 무엇이 작은 코드 조각에 해당하는지에 대한 견해에도 영향을 미친다.

각각의 모든 클래스를 격리해야 한다면 테스트 대상 코드 조각은 당연히 단일 클래스이거나 해당 클래스 내의 메서드여야 한다. 격리 문제에 접근하는 방식 때문에 이보다 더 클 수가 없다. 때에 따라 한 번에 몇 개의 클래스를 테스트하는 지침을 따르려고 노력해야 한다.

앞에서 언급했듯이 격리 특성을 해석하는 또 다른 방법으로 고전적인 방법이 있다. 고전적인 방법에서 코드를 꼭 격리하는 방식으로 테스트해야 하는 것이 아니다. 대신 단위 테스트는 서로 격리해서 실행해야 한다. 이렇게 하면 테스트를 어떤 순서로든 가장 적합한 방식으로 실행할 수 있으며 서로의 결과에 영향을 미치지 않는다.

각각의 테스트를 격리하는 것은 여러 클래스가 모두 메모리에 상주하고 공유 상태에 도달하지 않는 한, 여러 클래스를 한 번에 테스트해도 괜찮다는 뜻이다. 이를 통해 테스트가 서로 소통하고 실행 컨텍스트에 영향을 줄 수 있다. 데이터베이스, 파일 시스템 등 프로세스 외부 의존성이 이러한 공유 상태의 대표적인 예다.

예를 들어 어떤 테스트가 준비 단계에서 데이터베이스에서 고객을 생성할 수 있고, 이 테스트가 실행되기 전에 다른 테스트의 준비 단계에서 고객을 삭제할 수도 있다. 이 두 가지 테스트를 병렬로 실행하면 첫 번째 테스트가 실패하는데, 이는 제품 코드가 고장 나서가 아니라 두 번째 테스트의 간섭 때문이다.

격리 문제에 대한 이러한 견해는 목과 기타 테스트 대역의 사용에 대한 훨씬 더 평범한 견해를 수반한다. 테스트 대역을 사용할 수 있지만, 보통 테스트 간에 공유 상태를 일으키는 의존성에 대해서만 사용한다.

img

공유 의존성은 테스트 대상 클래스 간이 아니라 단위 테스트 간에 공유한다. 그런 의미에서 싱글턴 의존성은 각 테스트에서 새 인스턴스를 만들 수 있기만 하면 공유되지 않는다. 제품 코드에는 싱글턴 인스턴스가 단 하나만 있지만, 테스트는 이 패턴을 따르지 않고 재사용하지도 않는다. 따라서 이러한 의존성은 비공개인 것이다.

새 파일 시스템이나 데이터베이스를 만들 수는 없으며, 테스트 간에 공유되거나 테스트 대역으로 대치돼야 한다.

공유 의존성을 대체하는 또 다른 이유는 테스트 실행 속도를 높이는 데 있다. 공유 의존성은 거의 항상 실행 프로세스 외부에 있는 데 반해, 비공개 의존성은 보통 그 경계를 넘지 않는다. 따라서 데이터베이스나 파일 시스템 등의 공유 의존성에 대한 호출은 비공개 의존성에 대한 호출보다 더 오래 간다. 그리고 단위 테스트 두 번째 속성으로 빨리 실행해야하는 필요성이 있으므로, 이러한 호출을 포함하는 공유 의존성을 가진 테스트는 단위 테스트 영역에서 통합 테스트 영역으로 넘어간다.

공유 의존성이 없는 한 여러 클래스를 묶어 단위 테스트할 수도 있다.

2.2. 단위 테스트의 런던파와 고전파

런던파와 고전파로 나눠진 원인은 격리 특성에 있다. 런던파는 테스트 대상 시스템에서 협력자를 격리하는 것으로 보는 반면, 고전파는 단위 테스트끼리 격리하는 것으로 본다.

세 가지 주제에 대해 의견 차이가 있다.

  • 격리 요구 사항
  • 테스트 대상 코드 조각의 구성 요소
  • 의존성 처리
격리 주체단위의 크기테스트 대역 사용 대상
런던파단위단일 클래스불변 의존성 외 모든 의존성
고전파단위 테스트단일 클래스 또는 클래스 세트공유 의존성

2.2.1. 고전파와 런던파가 의존성을 다루는 방법

테스트 대역을 어디에서나 흔히 사용할 수 있지만, 런던파는 테스트에서 일부 의존성을 그대로 사용할 수 있도록 하고 있다. 절대 변하지 않는 객체, 즉 불변 객체는 교체하지 않아도 된다.

이전 예제에서 봤듯이, 테스트를 런던 스타일로 리팩터링하면 Product 인스턴스를 목으로 바꾸지 않고 실제 객체를 사용한다. Customer의 두 가지 의존성 중 Store만 시간에 따라 변할 수 있는 내부 상태를 포함하고 있다. Product 인스턴스는 불변이다. 따라서 Store 인스턴스만 교체했다.

이러한 불변 객체를 값 객체 또는 값이라고 한다. 주요 특징은 각각의 정체성이 없다는 것이다. 즉, 내용에 의해서만 식별된다. 그 결과, 두 객체가 동일한 내용을 갖고 있다면 어떤 객체를 사용하든 상관없다.

비공개 의존성은 변경 가능하거나 불변일 수 있다. 불변인 경우 값객체라고 부른다. 예를 들어 데이터베이스는 공유 의존성이며, 내부 상태는 모든 자동화된 테스트에서 공유한다. Store 인스턴스는 변경 가능한 비공개 의존성이다. 그리고 Product 인스턴스는 불변인 비공개 의존성. 즉 값 객체의 예다. 모든 공유 의존성은 변경 가능하지만, 변경 가능한 의존성을 공유하려면 여러 테스트에서 재사용돼야 한다.

img

협력자 대 의존성

협력자(collaborator)는 공유하거나 변경 가능한 의존성이다. 예를 들어, 데이터베이스는 공유 의존성이므로 데이터베이스 접근 권한을 제공하는 클래스는 협럭자다. Store도 시간에 따라 상태가 변할 수 있기 때문에 협럭자다.

Product와 숫자 5도 역시 의존성이지만 협력자는 아니다. 값 또는 값 객체로 분류된다.

일반적인 클래스는 두 가지 유형의 의존성으로 동작한다. 협력자와 값이다.

모든 프로세스 외부 의존성이 공유 의존성의 범주에 속하는 것은 아니다. 공유 의존성은 거의 항상 프로세스 외부에 있지만, 그 반대는 그렇지 않다. 프로세스 외부 의존성을 공유하려면 단위 테스트가 서로 통신할 수 있는 수단이 있어야 한다. 의존성 내부 상태를 수정하면 통신이 이뤄진다. 그런 의미에서 프로세스 외부의 불변 의존성은 그런 수단을 제공하지 않는다. 테스트는 내부의 어떤 것도 수정할 수 없기 때문에 서로 실행 컨텍스트에 영향을 줄 수 없다.

예를 들어 조직에서 판매하는 모든 제품에 대한 카탈로그를 반환하는 API가 있다면, API는 카탈로그를 변경하는 기능을 노출하지 않는 한 공유 의존성이 아니다. 이러한 의존성은 휘발성이고 애플리케이션 경계를 벗어나는 것이 사실이지만, 테스트가 반환하는 데이터에 영향을 미칠 수 없기 때문에 공유가 아니다. 대부분의 경우 테스트 속도를 높이려면 테스트 대역으로 교체해야 한다. 그러나 프로세스 외부 의존성이 충분히 빠르고 연결이 안정적이면 테스트에서 그대로 사용하는 것도 괜찮다.

실제 프로젝트에서 프로세스 외부가 아닌 공유 의존성은 거의 없다. 의존성이 프로세스 내부에 있으면 각 테스트에서 별도의 인스턴스를 쉽게 공급할 수 있으므로 테스트 간에 공유할 필요가 없다. 마찬가지로 공유되지 않는 프로세스 외부 의존성은 일반적으로 접할 일이 없다. 이러한 의존성 대부분은 변경 가능하며 테스트로 수정될 수 있다.

2.3. 고전파와 런던파 비교

고전파와 런던파 간의 주요 차이는 단위 테스트의 정의에서 격리 문제를 어떻게 다루는지에 있다.

개인적으로 단위 테스트 고전파를 선호한다. 이는 고품질의 테스트를 만들고 단위 테스트의 궁극적인 목표인 프로젝트의 지속 가능한 성장을 달성하는 데 더 적합하다. 그 이유는 취약성에 있다. 목을 사용하는 테스트는 고전적인 테스트보다 불안정한 경향이 있기 때문이다.

런던파의 접근 방식은 다음과 같은 이점을 제공한다.

  • 입자성이 좋다. 테스트가 세밀해서 한 번에 한 클래스만 확인한다.
  • 서로 연결된 클래스의 그래프가 켜져도 테스트하기 쉽다. 모든 협력자는 테스트 대역으로 대체되기 때문에 테스트 작성 시 걱정할 필요가 없다.
  • 테스트가 실패하면 어떤 기능이 실패했는지 확실히 알 수 있다. 클래스의 협력자가 없으면 테스트 대상 클래스 외에 다른 것을 의심할 여지가 없다.

2.3.1. 한 번에 한 클래스만 테스트하기

런던파는 클래스를 단위로 간주한다. 클래스를 테스트에서 검증할 원자 단위로도 취급하게 한다. 이런 경향은 이해되기는 하지만 오해의 소지가 있다. 그래서 좋은 코드 입자성을 목표로 하는 것은 도움이 되지 않는다. 테스트가 단일 동작 단위를 검증하는 한 좋은 테스트다. 이보다 적은 것을 목표로 삼는다면 사실 단위 테스트를 훼손하는 결과를 가져온다. 이 테스트가 무엇을 검증하는지 정확히 이해하기가 더 어려워지기 때문이다.

테스트는 코드의 단위를 검증해서는 안 된다. 오히려 동작의 단위, 즉 문제 영역에 의미가 있는 것. 이상적으로는 비즈니스 담당자가 유용하다고 인식할 수 있는 것을 검증해야 한다. 동작 단위를 구현하는 데 클래스가 얼마나 필요한지는 상관없다. 단위는 여러 클래스에 걸쳐 있거나 한 클래스에만 있을 수 있고, 심지어 아주 작은 메서드가 될 수도 있다.

테스트는 해결하는 데 도움이 되는 문제에 대한 이야기를 들려줘야 하며, 이 이야기는 프로그래머가 아닌 일반 사람들에게 응집도가 높고 의미가 있어야 한다. 예를 들어 다음은 응집도가 높은 이야기의 예다.

우리집 강아지를 부르면, 바로 나에게 온다.

이제 다음과 비교해보자.

우리집 강아지를 부르면 먼저 왼쪽 앞다리를 움직이고, 이어서 오른쪽 앞다리를 움직이고, 머리를 돌리고, 꼬리를 흔들기 시작한다.

두 번째 이야기는 훨씬 말이 안된다. 저 움직임은 모두 무엇인가? 강아지가 나에게 오고 있는가? 아니면 도망을 가고 있는가? 실제 동작 대신 개별 클래스를 목표로 할 때 테스트가 이렇게 보이기 시작한다.

2.3.2. 상호 연결된 클래스의 큰 그래프를 단위 테스트하기

실제 협력자를 대신해 목을 사용하면 클래스를 쉽게 테스트할 수 있다. 특히 테스트 대상 클래스에 의존성이 있고, 이 의존성에 다시 각각의 의존성이 있고, 이렇게 여러 계층에 걸쳐서 계속되는 식으로 의존성 그래프가 복잡하게 있을 때 쉽게 테스트할 수 있다. 테스트 대역을 쓰면 클래스의 직접적인 의존성을 대체해 그래프를 나눌 수 있으며, 이는 단위 테스트에서 준비해야 할 작업량을 크게 줄일 수 있다. 고전파를 따라 테스트 대상 시스템을 설정하려면 전체 객체 그래프를 다시 생성해야 하는데, 작업이 많을 수 있다.

모두 사실이지만, 상호 연결된 클래스의 크고 복잡한 그래프를 테스트할 방법을 찾는 대신, 먼저 이러한 클래스 그래프를 갖지 않는 데 집중해야 한다. 대개 클래스 그래프가 커진 것은 코드 설계 문제의 결과다.

테스트에서 이 문제를 지적한 것은 사실 좋은 일이다. 비교적 높은 정확도로 저품질을 예측한다. 클래스를 단위 테스트하려면 테스트 준비 단계를 적정선을 넘게 늘려야 해서 이는 틀림없이 문제의 징후가 있다. 목을 사용하는 것은 이 문제를 감추기만 할 뿐, 원인을 해결하지 못한다.

2.3.3. 버그 위치 정확히 찾아내기

런던 스타일 테스트가 있는 시스템에 버그가 생기면, 보통 sut에 버그가 포함된 테스트만 실패한다. 하지만 고전적인 방식이면, 오작동하는 클래스를 참조하는 클라이언트를 대상으로 하는 테스트도 실패할 수 있다. 즉, 하나의 버그가 전체 시스템에 걸쳐 테스트 실패를 야기하는 파급 효과를 초래한다. 결국 문제의 원인을 찾기가 더 어려워진다. 문제를 파악하고자 테스트를 디버깅하는 데 시간이 걸릴 수 있다.

우려할 만하지만, 큰 문제는 아니다. 테스트를 정기적으로 실행하면 버그의 원인을 알아낼 수 있다. 즉, 마지막으로 한 수정이 무엇인지 알기 때문에 문제를 찾는 것은 그리 어렵지 않다. 또한 실패한 테스트를 모두 볼 필요는 없다. 하나를 고치면 다른 것들도 자동으로 고쳐진다.

테스트 스위트 전체에 걸쳐 계단식으로 실패하는 데 가치가 있다. 버그가 테스트 하나뿐만 아니라 많은 테스트에서 결함으로 이어진다면, 방금 고장 낸 코드 조각이 큰 가치가 있다는 것을 보여준다. 즉, 전체 시스템이 그것에 의존한다. 유요한 정보다.

2.3.4. 고전파와 런던파 사이의 다른 차이점

고전파와 런던파 사이에 남아있는 두 가지 차이점은 다음과 같다.

  • 테스트 주도 개발을 통한 시스템 설계 방식
  • 과도한 명세 문제

런던 스타일의 단위 테스트는 하향식 TDD로 이어지며, 전체 시스템에 대한 기대를 설정하는 상위 레벨 테스트부터 시작한다. 목을 사용해 예상 결과를 달성하고자 시스템이 통신해야 하는 협력자를 지정한다. 그런 다음 모든 클래스를 구현할 때까지 클래스 그래프를 다져나간다. 목은 한 번에 한 클래스에 집중할 수 있기 때문에 이 설계 프로세스를 가능하게 한다.

고전파는 테스트에서 실제 객체를 다뤄야 하기 때문에 지침을 똑같이 두지 않는다. 대신 일반적으로 상향식으로 한다. 고전적 스타일에서는 도메인 모델을 시작으로 최종 사용자가 소프트웨어를 사용할 수 있을 때까지 계층을 그 위에 더 둔다.

런던 스타일은 고전 스타일보다 테스트가 구현에 더 자주 결합되는 편이다. 이로 인해 런던 스타일과 목을 전반적으로 아무 데나 쓰는 것에 대해 주로 이의가 제기된다.

2.4. 두 분파의 통합 테스트

런던파는 실제 협력자 객체를 사용하는 모든 테스트를 통합 테스트로 간주한다. 고전 스타일로 작성된 대부분의 테스트는 런던파 지지자들에게 통합 테스트로 느껴질 것이다.

단위 테스트는 다음과 같은 특징이 있는 자동화된 테스트다.

  • 작은 코드 조각을 검증하고
  • 빠르게 수행하고
  • 격리된 방식으로 처리한다.

고전파 관점에서 다시 정의한 단위 테스트는

  • 단일 동작 단위를 검증하고
  • 빠르게 수행하고
  • 다른 테스트와 별도로 처리한다.

통합 테스트는 이러한 기준 중 하나를 충족하지 않는 테스트다. 예를 들어 공유 의존성에 접근하는 테스트는 다른 테스트와 분리해 실행할 수 없다.

둘 이상의 동작 단위를 검증할 때의 테스트는 통합 테스트다. 이는 종종 테스트 스위트의 실행 속도를 최적화하려는 노력의 결과다. 비슷한 단계를 따르지만 다른 동작 단위를 검증하는 느린 테스트가 두 개 있을 때, 하나로 합치는 것이 타당할 수 있다.

또한 다른 팀이 개발한 모듈이 둘 이상 있을 때 통합 테스트로 어떻게 작동하는지 검증할 수 있다.

2.4.1. 통합 테스트의 일부인 엔드 투 엔드 테스트

간단히 말해 통합 테스트는 공유 의존성, 프로세스 외부 의존성뿐 아니라 조직 내 다른 팀이 개발한 코드 등과 통합해 작동하는지도 검증하는 테스트다. 엔드 투 엔드 테스트라는 개념도 따로 있다. e2e는 통합 테스트의 일부다. e2e 테스트도 코드가 프로세스 외부 종속성과 함께 어떻게 작동하는지 검증한다. e2e 테스트와 통합 테스트 간의 차이점은 e2e가 일반적으로 의존성을 더 많이 포함한다는 것이다.

가끔 경계가 흐리지만, 일반적으로 통합 테스트는 프로세스 외부 의존성을 한두 개만 갖고 작동한다. 반면에 e2e 테스트는 프로세스 외부 의존성을 전부 또는 대다수 갖고 작동한다. 따라서 e2e는 모든 외부 애플리케이션을 포함해 시스템을 최종 사용자의 관점에서 검증하는 것을 의미한다.

e2e 테스트는 유지 보수 측면에서 가장 비용이 많이 들기 때문에 모든 단위 테스트와 통합 테스트를 통과한 후 빌드 프로세스 후반에 실행하는 것이 좋다.

e2e 테스트도 테스트 대역을 사용할 수도 있고 통합 테스트와 뚜렸한 경계가 없다.

요약

  • 단위 테스트의 정의
    • 단일 동작 단위를 검증하고
    • 빠르게 수행하고
    • 다른 테스트와 별도로 처리한다.
  • 고전파와 런던파는 무엇이 단위를 의미하는지에 대한 관점과 테스트 대상 시스템의 의존성 처리 방식이 다르다.
    • 런던파는 테스트 대상 단위를 서로 분리해야 한다고 한다. 테스트 대상 단위는 코드의 단위, 보통 단일 클래스다. 불변 의존성을 제외한 모든 의존성을 테스트 대역으로 대체해야 한다.
    • 고전파는 단위가 아니라 단위 테스트를 서로 분리해야 한다고 한다. 또한 테스트 대상 단위는 코드 단위가 아니라 동작 단위다. 따라서 공유 의존성만 테스트 대역으로 대체해야 한다. 공유 의존성은 테스트가 서로 실행 흐름에 영향을 미치는 수단을 제공하는 의존성이다.
  • 런던파는 더 나은 입자성의 이점, 상호 연결된 클래스의 큰 그래프에 대한 테스트 용이성 그리고 테스트 실패 후 버그가 있는 기능을 쉽게 찾을 수 있는 편의성 등을 제공한다.
  • 런던파의 장점이 처음에는 매력적으로 보인다. 그러나 몇 가지 문제가 있다. 테스트는 코드 단위가 아니라 동작 단위를 검증해야 한다. 코드 조각을 단위 테스트할 수 없다는 것은 코드 설계에 문제가 있다는 사실을 알려주는 강한 징후다. 테스트 대역을 사용한다고 해도 이 문제를 해결하는 게 아니라 오히려 숨길 뿐이다. 마지막으로 테스트 실패 후 어떤 기능에 버그가 있는지 판단하는 것이 도움은 되지만, 종종 버그의 원인을 알고 있기 때문에 그리 큰 문제는 아니다. 마지막에 수정한 것이 버그의 원인이다.
  • 런던파 테스트의 가장 큰 문제는 과잉 명세, 세부 구현에 결합된 테스트 문제다.
  • 통합 테스트는 단위 테스트 기준 중 하나 이상을 충족하지 못하는 테스트다. e2e 테스트는 통합 테스트의 일부다. 최종 사용자의 관점에서 시스템을 검증한다. e2e 테스트는 애플리케이션과 함께 작동하는 프로세스 외부 의존성의 전부 또는 대부분에 직접 접근한다.