단위 테스트가 의미 있는 코드 유형은 뭘까? 통합 테스트는?
’단위 테스트 (블라디미르 코리코프, 2021)’ 책의 ’7장 가치 있는 단위 테스트를 위한 리팩터링’을 보고 정리한 내용이다. 모든 코드를 테스트할 수는 없다. 그래서 중요하고 의미 있는 영역부터 테스트를 짜야 한다. 중요한 영역부터 시작해 덜 중요한 영역까지 테스트 커버리지를 넓히는 전략을 선택해야 한다. 모든 코드를 테스트할 수는 없기에 합리적인 선택을 해야 한다.
코드의 네 가지 유형
모든 코드를 테스트할 수는 없다. 테스트 코드 또한 유지보수 대상이다. 그래서 최소한의 유지비로 최대의 가치를 끌어낼 수 있어야 한다. 테스트를 짜면 가장 효용성이 높은 테스트 대상 코드는 무엇인가? 찾는데 제품 코드를 2차원으로 분류해 보는 게 도움이 될 수 있다.
복잡도 또는 도메인 유의성(domain significance)이 y축이 되고 협력자(collaborator) 수가 x축이 된다.
복잡도는 코드 내 의사 결정 지점 수로 정의한다. 분기가 많아질수록 복잡도는 더 높아지게 된다. 도메인 유의성은 코드가 도메인에 대해 얼마나 의미가 있는지를 나타낸다. 도메인 계층의 코드는 유틸리티 코드와 다르게 도메인 유의성이 높다.
협력자 수는 클래스 또는 메서드가 가진 협력자 수이다. 많을수록 테스트 비용이 많이 든다.
도메인 유의성과 협력자 수로 네 가지 코드 유형을 나눌 수 있다.
- 도메인 모델과 알고리즘: 보통 복잡한 코드는 도메인 모델이지만, 100%는 아니다. 문제 도메인과 직접적으로 관련이 없는 복잡한 알고리즘이 있을 수 있다.
- 간단한 코드: C#에서 이러한 코드의 예로 매개변수가 없는 생성자와 한 줄 속성 등이 있다. 협력자가 있는 경우가 거의 없고 복잡도나 도메인 유의성도 거의 없다.
- 컨트롤러: 이 코드는 복잡하거나 비즈니스에 중요한 작업을 하는 것이 아니라 도메인 클래스와 외부 애플리케이션 같은 다른 구성 요소의 작업을 조정한다.
- 지나치게 복잡한 코드: 이러한 코드는 두 가지 지표 모두 높다. 협력자가 많으며 복잡하거나 중요하다. 한 가지 예로 덩치가 큰 컨트롤러(복잡한 작업을 어디에도 위임하지 않고 모든 것을 스스로 하는 컨트롤러)가 있다.
’도메인 모델과 알고리즘’ 유형이 단위 테스트 노력 대비 가장 이롭다. 최우선으로 단위 테스트를 짜야 하는 유형이다.
외부 라이브러리는 어디에 포함할 수 있을까? ’간단한 코드’ 유형에 포함할 수 있다. 예를 들어 JSON 직렬화 외부 라이브러리를 쓴 직렬화 코드를 따로 테스트할 필요는 없다. 근질거리면 차라리 외부 라이브러리가 단위 테스트가 잘 되어 있는지 확인하고 부족한 부분에 대한 테스트를 추가하자.
’지나치게 복잡한 코드’는 이상적으로 ’도메인 모델과 알고리즘’과 ’컨트롤러’로 분해할 수 있다. 쪼개는 과정에서 험블 객체(humble object) 패턴이 유용하다.
’컨트롤러’는 단위 테스트 대상에서 과감히 제외하자. 그럼 그대로 놔두나?
비즈니스 로직과 오케스트레이션을 분리
비즈니스 로직(business logic)과 오케스트레이션(orchestration)을 분리하는 경우다. 코드의 깊이와 코드의 너비 관점에서 이 두 가지 책임을 생각해볼 수 있다. 코드가 깊거나(복잡하거나 중요함) 넓을(많은 협력자와 작동함) 수 있지만, 둘 다 가능하지는 않다.
’도메인 모델과 알고리즘’ 유형이 비즈니스 로직에 해당하고 오케스트레이션이 ’컨트롤러’에 해당한다. ’지나치게 복잡한 코드’는 둘 다 가능한 코드도 깊고 넓은 유형이 된다.
실제로 통합 테스트(integration testing)는 대부분 시스템이 프로세스 외부 의존성과 통합해 어떻게 작동하는지를 검증한다. 다시 말해, 이 테스트는 ’컨트롤러’ 사분면에 속하는 코드를 다룬다. [.] 단위 테스트는 도메인 모델을 다루는 반면, 통합 테스트는 프로세스 외부 의존성과 도메인 모델을 연결하는 코드를 확인한다.
’컨트롤러’ 유형의 코드는 통합 테스트에서 테스트하면 된다. 예전 기억이 난다. 목(mock)을 이렇게 많이 썼는데, 이 테스트가 의미가 있을까? 이런 생각이 드는 테스트가 있었다. 지금 생각하면 ’컨트롤러’ 유형을 우격다짐으로 단위 테스트를 한 것 같다.
비즈니스 로직과 오케스트레이션을 분리하면 테스트가 용이해진다. 비즈니스 로직은 단위 테스트 대상이고 오케스트레이션은 통합 테스트 대상이다.
마치며
도메인 유의성과 협력자 수로 ’도메인 모델과 알고리즘’, ’간단한 코드’, ’컨트롤러’, ’지나치게 복잡한 코드’ 유형으로 나눌 수 있다. 단위 테스트 대상은 ’도메인 모델과 알고리즘’ 유형이고 통합 테스트 대상은 ’컨트롤러’ 유형이다. ’지나치게 복잡한 코드’는 ’도메인 모델과 알고리즘’과 ’컨트롤러’로 쪼갤 수 있다. 쪼개는데 험블 객체(humble object) 패턴이 도움이 된다.
고수가 테스트 안 해도 되는 걸 과감하게 잘라주는 게 좋았다. 테스트 용이성이 아키텍처에 영향을 많이 준다는 것을 다시 한번 더 느낄 수 있었다. 비즈니스 로직과 오케스트레이션 분리는 MVC(Model-View-Controller)부터 육각형 아키텍처(hexagonal architecture)에 이르기까지 널리 차용되는 개념이다. 이게 코드 복잡도 관리에 도움을 준다. 여기에서 그치는 게 아니라 단위 테스트와 통합 테스트 대상을 구분 지어서 테스트 용이성까지 높여준다.