[책 공부] 단위테스트

4 minute read

책 소개

단위테스트

이 책은 테스트에 대해 조금이라도 공부를 해본 사람이라면 추천합니다. 단위테스트를 왜 작성해야하는지, 어떤 구조로 작성해야 좋은지를 굉장히 잘 설명하고 있습니다. 테스트코드를 작성함으로써 어떻게 지속 가능한 성장을 이룰 수 있는지를 회귀테스트, 리팩토링, 거짓 양성 등 다양한 개념을 도출하여 설명하고 있습니다. 또한 테스트코드를 잘 작성하는 것이 도메인 계층 간에 관심사를 분리하여 더욱 유지보수하기가 좋아지는 코드를 만들 수 있습니다.

단위테스트 현황

단위테스트의 목표는 소프트웨어 프로젝트의 지속 가능한 성장을 가능하게 하는 것이다. 지속 가능하다는 것이 단위테스트의 핵심이다. 테스트코드의 커버리지를 나타내는 지표가 있는데 반드시 100%일 필요는 없다. 너무 적으면 테스트코드가 부족하다는 문제가 되지만 100%에 가깝다고 좋은 테스트코드는 아니다. 100%에 가깝게 만드려고 오히려 시스템을 속일 방법을 찾기 시작하며 좋은 코드를 고려하지 않은채 테스트 통과만 된다고 생각한다. (Mocking만 계속 하는 것이다.) 테스트코드 작성의 목표를 잃은 것이다. 80%만 채우더라도 핵심 도메인 로직 위주로 잘 작성하는 것이 중요하다.

좋은 단위 테스트 스위트는 개발 속도를 지키면서 침체 단계에 빠지지 않게 한다. 이러한 테스트 스위트가 있다면 변경 사항이 회귀로 이어지지 않을 것이라고 확신해도 좋다. 또한 코드를 리팩토링하거나 새로운 기능을 추가하는 것이 더 쉬워진다.

단위테스트란

단위테스트란

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

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

고전파(Detrioit)

  • 실제 협력 객체를 최대한 사용
  • 테스트 대상 시스템(SUT)과 그 의존성을 함께 테스트하는 것을 선호
  • Mock은 외부 시스템(DB, API 등)이나 공유 의존성에만 제한적으로 사용
  • 테스트가 실제 운영 환경과 유사하게 동작하는 것을 중시 (예시: 계산기 클래스가 로그 객체를 사용한다면, 실제 로그를 함께 테스트합니다)

런던파 (Mockist)

  • Mock 객체를 적극적으로 활용
  • 테스트 대상을 최대한 격리시켜 테스트하는 것을 선호
  • 모든 의존성을 Mock으로 대체하여 순수하게 하나의 클래스만 테스트
  • 객체 간 상호작용과 메시지 전달을 검증하는 데 중점을 둔다 (예: 계산기 클래스를 테스트할 때 로그를 Mock으로 만들고, 로그의 특정 메서드가 호출되었는지 검증합니다)

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

테스트를 작성하는데 단순히 검증하는 것을 넘어서 코드 리팩토링, 유지보수 가능한 코드를 만들게 해준다. 이러한 점에서 고전파로 테스트를 작성하는 것이 더 수월하다. 특히 런던파의 가장 큰 문제는 과잉 명세, 즉 SUT 세부 구현에 결합된 테스트 문제다. 이것은 구현에 종속되어 있기 때문에 기능이 똑같아도 리팩토링을 하게되면 테스트가 깨지는 현상이 발생한다. 또한 Mock은 테스트대역을 만들어서 테스트가 항상 성공하도록 설정되어 있어서 객체간에 제대로 협력하지 못해도 테스트는 통과하는 현상이 있다.

단위테스트 구조

모든 테스트는 AAA 패턴(준비, 실행, 검증)을 따라야 한다.

테스트에서 준비, 실행 또는 검증 구절이 여러 개 있는 테스트를 볼 수 있다. 즉, 여러 개의 동작 단위를 검증하는 하나의 테스트를 의미하는데 이것은 단위테스트가 아닌 통합테스트로 피하는 것이 좋다. 각 동작을 고유의 테스트로 도출해야 한다. 그리고 테스트 내 if, for문 등은 피해야 한다. 준비 구절이 큰 경우에는 팩토리 클래스를 생성하여 분리시키는 것도 좋다.

테스트 픽스처 재사용

테스트 픽스처는 테스트 실행 대상 객체다. 테스트를 실행하기 위해 필요한 준비물을 말하는데 이러한 테스트 픽스처는 테스트 간에 재사용하는 것이 좋다. 자바 언어에서는 @Before 이라는 애노테이션을 제공해준다. 특히 테스트 간에는 모두 독립적으로 실행되어야 하며 절대 결합되서는 안된다.

  • 테스트 픽스처 초기화 코드는 생성자에 두지 말고 팩토리 메서드를 도입해서 재사용하자.

테스트 대역

테스트 대역은 모든 유형의 비운영용 가짜 의존성을 설명하는 포괄적인 용어다. 테스트 대역의 주 용도는 테스트를 편리하게 하는 것이다. 테스트 대상 시스템으로 실제 의존성 대신 전달되므로 설정이나 유지보수가 어려울 수 있다.

테스트 대역에는 더미(dummy), 스텁(stub), 스파이(spy), 목(mock), 페이크(fake)가 있다.

  • 더미(dummy): 아무 동작도 안 하고, 그냥 파라미터를 채우기 위해 전달되는 객체
  • 스텁(stub): 미리 준비된 답변을 반환하는 객체. 질문하면 항상 같은 대답만 한다.
  • 스파이(spy): Stub처럼 답변도 하지만, 호출 내역을 몰래 기록한다. 나중에 확인 가능하다.
  • 목(mock): 예상되는 호출을 미리 프로그래밍하고, 그대로 호출됐는지 검증한다.
  • 페이크(fake): 실제로 동작하는 간단한 구현체. 진짜처럼 작동하지만 운영에는 사용하지 못한다.

목은 SUT에서 관련 의존성으로 나가는 상호작업을 모방하고 검사하는 방면, 스텁은 내부로 들어오는 상호작업만 모방하고 검사하지는 않는다. SUT에서 스텁으로의 호출은 SUT가 생성하는 최종결과가 아니다. 이러한 호출은 최종결과를 산출하기 위한 수단일 뿐이다. 즉, 스텁은 SUT가 출력을 생성하도록 입력을 제공하는 것이지 스텁으로 상호작용을 검증하면 안된다. 스텁 호출을 검증하면 ‘과잉 명세’ 문제가 생긴다.

최종 결과가 아닌 사항을 검증하는 이러한 관행을 과잉 명세(overspecification)이라 한다.

SMTP 같은 외부서비스에 대한 호출을 목으로 하는 이유는 타당하다. 리팩토링 후에도 통신 유형이 그대로 유지되도록 하기 때문에 테스트 취약성을 야기하지 않는다. 이메일 발송은 애플리케이션의 최종 목적이다. 사이드 이펙트이자 관찰 가능한 결과물이다. 그 밖에 메시지 큐 발생, 외부 API 호출 등이 목을 사용해도 괜찮다.

애플리케이션에는 시스템 내부 통신과 시스템 간 통신이라는 두가지 유형이 있다. 시스템 내부 통신은 애플리케이션 내 클래스 간의 통신이다. 시스템 간 통신은 애플리케이션이 외부 애플리케이션과 통신할 때는 말한다. 시스템 내 통신을 검증하고자 목을 사용하면 취약한 테스트로 이어진다. 시스템 내 통신은 구현 세부 사항이기 때문이다. 따라서 시스템 간 통신(애플리케이션 경계를 넘는 통신)과 해당 통신의 사이드 이펙트가 외부 환경에서 보일 때만 목을 사용하는 것이 타당하다.

통합테스트

단위테스트에만 전적으로 의존하면 시스템이 전체적으로 잘 작동하는지 확신할 수 없다. 데이터베이스나 메시지 버스 등의 외부 시스템과 어떻게 통합되는지 확인해야 한다.

단위테스트가 아닌 모든 테스트가 바로 통합테스트에 해당된다. 대부분은 단위테스트로 작성하고, 중요한 통합테스트가 비즈니스 시나리오당 하나, 두개정도 있으면 시스템 전체의 정확도를 보장할 수 있다.

통합테스트는 컨트롤러를 다루고, 단위테스트는 알고리즘과 도메인 모델을 다룬다.

통합테스트에서만 목을 사용하고 단위테스트에서는 사용하지 말아야 한다. 목은 바로 통합테스트만을 위한 것이다. 이 규칙을 적용하면 자연스레 도메인 모델과 컨트롤러라는 고유 계층 두개로 만들어지게 된다.

Comments