최근에 글또의 개발자분들과 <단위 테스트 - 블라디미르 코리코프> 책 스터디를 진행했다.
파편적으로 알고있던 단위 테스트에 대한 지식들과 몰랐던 지식까지 한번 정리를 할 수 있어서 도움이 많이 되었다.
마침 최근에 회사에서도 단위 테스트 작성을 적극적으로 도입하고 있어서 열심히 적용해 볼 예정이다.
스터디를 하면서 개인 노트에 정리를 했지만, 더 자주 보고 익히기 위해서 블로그에도 정리를 한다.
1. 단위 테스트의 목표
1.1 단위 테스트 현황
- 단위 테스트에 시간을 투자할 때는 항상 최대한 이득을 얻도록 노력해야하며, 테스트에 드는 노력을 가능한 줄이고 그에 따르는 이득을 최대화해야 한다.
- 좋은 테스트와 좋지 않은 테스트의 차이는 취향이나 개인적인 선호도의 문제가 아니라 현재 작업 중인 중대한 프로젝트의 성패를 가르는 문제다.
1.2 단위 테스트의 목표
- 비교적 높은 정확도로 저품질의 코드를 가려낸다. 코드를 단위 테스트하기 어렵다면 코드 개선이 반드시 필요하다는 것을 의미한다. 보통 강결합에서 저품질이 나타나는데, 여기서 강결합은 제품 코드가 서로 충분히 분리되지 않아서 따로 테스트하기 어려움을 뜻한다.
- 단위 테스트의 목표는? 소프트웨어 프로젝트의 지속 가능한 성장을 가능하게 하는 것이다.
- 지속적인 정리와 리팩터링 등과 같은 적절한 관리를 하지 않고 방치하면 시스템이 점점 더 복잡해지고 무질서해진다.
- 모든 테스트를 작성할 필요는 없다. 일부 테스트는 아주 중요하고 소프트웨어 품질에 매우 많은 기여를 한다. 그 밖에 다른 테스트는 그렇지 않다.
- 기반 코드가 어떻게 동작하는지 이해하려고 할 때는 테스트를 읽는 데 시간을 투자하라.
1.3 테스트 스위트 품질 측정을 위한 커버리지 지표
- 단지 코드 경로를 통과하는 것이 아니라 실제로 테스트하려면, 단위 테스트에는 반드시 적절한 검증이 있어야 한다.
1.4 무엇이 성공적인 테스트 스위트를 만드는가
테스트 스위트의 품질을 어떻게 측정해야 하는가? 믿을 만한 방법은 스위트 내 각 테스트를 하나씩 따로 평가하는 것 뿐이다. 성고엊깅 테스트 스위트는 다음과 같은 특성을 가지고 있다.
- 개발 주기에 통합되어 있다.
- 이상적으로는 코드가 변경될 때마다 아무리 작은 것이라도 실행해야 한다.
- 코드베이스에서 가장 중요한 부분만을 대상으로 한다.
- 비즈니스 로직 테스트가 시간 투자 대비 최고의 수익을 낼 수 있다.
- 이 지침을 따르려면 도메인 모델을 코드베이스 중 중요하지 않은 부분과 분리해야 한다. 도메인 모델을 다른 애플리케이션 문제와 분리해야 단위 테스트에 대한 노력을 도메인 모델에만 집중할 수 있다.
- 최소한의 유지비로 최대의 가치를 끌어낸다.
- 가치 있는 테스트를 작성하려면 코드 설계 기술도 알아야 한다. 단위 테스트와 기반 코드를 서로 얽혀 있으므로 코드베이스에 노력을 많이 기울이지 않으면 가치 있는 테스트를 만들 수 없다.
2. 단위 테스트란 무엇인가
2.1 단위 테스트의 정의
단위 테스트는 가장 중요한 세가지 속성이 있다.
- 작은 코드 조각(단위)을 검증한다.
- 빠르게 수행한다.
- 격리된 방식으로 처리하는 자동화된 테스트다.
격리 문제에 대한 런던파(Mockist)의 접근
- 코드 조각을 격리된 방식으로 검증한다는 것은 테스트 대상 시스템을 협력자(collaborator)에게서 격리하는 것을 일컫는다. 즉 하나의 클래스가 다른 클래스에 의존하면 이 모든 의존성을 테스트 대역으로 대체해야 한다.
- 아래 테스트 케이스는 고전파(Classicist) 형식을 따른 예시이다.
public void purchase_succeeds_when_enough_inventory() {
// arrange
const store = new Store();
store.addInventory(Product.Shampoo, 10);
const customer = new Customer();
// act
const success = customer.purchase(store, Product.Shampoo, 5);
// assert
Assert.True(success);
Assert.Equal(5, store.getInventory(Product.Shampoo));
}
-> 테스트 대상 클래스(SUT)는 `Customer`, 협력자는 `Store` 이다.
-> 테스트는 `Store`를 대체하지 않고 운영용 인스턴스를 사용한다.
-> 이 테스트에서 두 클래스는 서로 격리되어 있지 않다.
- 아래 테스트 케이스는 위의 예시를 런던파 형식을 따라 작성한 것이다.
public void purchase_succeeds_when_enough_inventory() {
// arrange
const storeMock = new Mock<IStore>();
storeMock
.setUp(x => x.hasEnoghInventory(Product.Shampoo, 5))
.returns(true);
// act
const success = customer.purchase(storeMock.Object, Product.Shampoo, 5);
// assert
Assert.True(success);
storeMock.verify(x => x.removeInventory(Product.Shampoo, 5), Times.Once);
}
-> `Store`를 실제 인스턴스를 생성하지 않고 목을 생성해 대체한다.
-> 이전에는 상점의 상태를 검증했고 지금은 `Customer`와 `Store`간의 상호 작용을 검사한다.
격리 문제에 대한 고전파(Classicist)의 접근
- 코드를 꼭 격리하는 방식으로 테스트해야 하는 것은 아니다. 대신 단위테스트는 서로 격리해서 실행해야한다. 이렇게 하면 테스트를 어떤 순서로든 가장 적합한 방식으로 실행할 수 있으며 서로의 결과에 영향을 미치지 않는다.
- 고전파에서는 테스트 대역을 사용할 수는 있지만 보통 테스트 간에 공유 상태를 일으키는 의존성(database 등)에 대해서만 사용한다.
- 단위가 반드시 클래스에 국한될 필요는 없다. 공유 의존성이 없는 한 여러 클래스를 묶어서 단위 테스트할 수도 있다.
2.2 단위 테스트의 런던파와 고전파
- 런던파와 고전파로 나눠진 원인은 격리 특성에 있다.
격리 주체 | 단위의 크기 | 테스트 대역 사용 대상 | |
런던파 | 단위 | 단일 클래스 | 불변 의존성 외 모든 의존성 |
고전파 | 단위 테스트 | 단일 클래스 또는 클래스 세트 | 공유 의존성 |
- 불변 객체는 테스트 대역으로 교체하지 않아도 된다.
- 불변 객체를 값 객체(Value Object)라고 한다. 주요 특징은 각각의 정체성이 없다는 것이다. 즉, 내용에 의해서만 식별된다. 두 객체가 동일한 내용을 갖고 있다면 어떤 객체를 사용하든 상관없다.
- 일반적인 클래스는 두가지 유형의 의존성으로 동작한다. 협력자와 값이다. 협력자는 공유 의존성이거나 변경 가능한 의존성이다.
2.3 고전파와 런던파의 비교
런던파의 접근 방식의 이점
- 입자성이 좋다. 테스트가 세밀해서 한번에 한 클래스만 확인한다.
- 좋은 코드 입자성을 목표로 하는 것은 도움이 되지 않는다. 테스트가 단일 동작 단위를 검증하는 한 좋은 테스트다. 테스트는 해결하는 데 도움이 되는 문제에 대한 이야기를 들려줘야 하며 이 이야기는 프로그래머가 아닌 일반 사람들에게 응집도가 높고 의미가 있어야 한다.
- 서로 연결된 클래스의 그래프가 커져도 테스트하기 쉽다. 모든 협력자는 테스트 대역으로 대체되기 때문에 테스트 작성 시 걱정할 필요가 없다.
- 상호 연결된 클래스의 크고 복잡한 그래프를 테스트할 방법을 찾는 대신 먼저 이러한 클래스 그래프를 갖지 않는 데 집중해야 한다. 목을 사용하는 것은 이 문제를 감추기만 할 뿐, 원인을 해결하지 못한다.
- 테스트가 실패하면 어떤 기능이 실패했는지 확실히 알 수 있다.