# 단위 테스트

# 1.1 단위 테스트 현황

단위 테스트와 통합 테스트를 통해 좋은 코드 커버리지를 달성해야함. 테스트 작성에서 그치는 것이 아닌 노력 대비 최대 이익을 끌어내는 방식으로 단위 테스트를 수행해야 하는 것.

# 1.2 단위 테스트 목표

  • 소프트웨어 프로젝트의 지속 가능한 성장을 가능하게 하는 것.
  • 테스트가 없다면 개발 속도가 빠르게 감소하는 현상인 엔트로피가 나타난다.
  • 지속적인 정리와 리팩터링이 없다면 코드는 점점 무질서도(엔트로피)가 증가하여 시스템이 복잡해지고 무질서해진다.
  • 이러한 경향을 테스트로 뒤집을 수 있다. 안전망 역할을 하며 회귀에 대한 보험을 제공하는 도구이다. 테스트는 새로운 기능을 도입하거나 새로운 요구 사항에 더 잘 맞게 리팩터링 한 후에도 기존 기능이 잘 동작하는지 확인하는 데 도움이 된다.

단점도 생각해 볼 수 있는데 이러한 테스트는 초반에 상당한 노력이 필요하다는 것이다. 그러나 프로젝트 후반까지 장기적으로 보면 그 비용을 메울 수 있다. 코드베이스를 지속적으로 검증하는 테스트 없이는 소프트웨어 개발이 쉽게 확장하기 어렵다. 지속성과 확정성이 핵심이며, 이를 통해 장기적으로 개발 속도를 유지할 수 있다.

# 1.3 테스트 스위트 품질 측정을 위한 커버리지 지표

커버리지 지표는 테스트 스위트가 소스 코드를 얼마나 실행하는지를 백분율로 나타낸다.

# 1.3.1 코드 커버리지

코드 커버리지(테스트 커버리지) = 제품 코드 라인 수 / 전체 라인 수

# 1.3.2 분기 커버리지

분기 커버리지 = 통과 분기 / 전체 분기 수

# 1.3.3 커버리지 지표에 관한 문제점

  • 테스트 대상 시스템의 모든 가능한 결과를 검증한다고 보장할 수 없다.
  • 외부 라이브러리의 코드 경로를 고려할 수 있는 커버리지 지표는 없다.

# 1.3.4 특정 커버리지 숫자를 목표로 하기

커버리지 지표는 좋은 부정 지표지만 나쁜 긍정 지표다. 커버리지 숫자가 낮으면(예: 60%미만) 문제 징후라고 할 수 있다. 코드베이스에 테스트되지 않은 코드가 많다는 뜻이다. 그러나 높은 숫자도 별 의미가 없다. 그러므로 코드 커버리지 측정은 품질 테스트 스위트로 가는 첫 단계일 뿐임.

# 1.4 무엇이 성공적인 테스트 스위트를 만드는가?

  • 개발 주기에 통합돼 있다.
  • 코드베이스에서 가장 중요한 부분만을 대상으로 한다.
  • 최소한의 유지비로 최대의 가치를 끌어낸다.

# 1.4.1 개발 주기에 통합돼 있음

자동화된 테스트를 할 수 있는 방법은 끊임없이 하는 것 뿐. 모든 테스트는 개발 주기에 통합돼야 한다. 코드가 변경될 때마다 아무리 작은 것이라도 실행해야 한다.

# 1.4.2 코드베이스에서 가장 중요한 부분만을 대상으로 함

테스트의 가치는 테스트 구조뿐만 아니라 검증하는 코드에도 있다. 코드베이스 모든 부분에 주목할 필요는 없다. 즉, 시스템의 가장 중요한 부분에 단위 테스트 노력을 기울이고, 다른 부분은 간략하게 또는 간접적으로 검증하는 것이 좋다. 대부분 애플리케이션에서 가장 중요한 부분은 비즈니스 로직(도메인 모델)이 있는 부분이다. 비즈니스 로직 테스트가 시간 투자 대비 최고의 수익을 낼 수 있다. 다른 모든 부분은 세 가지 범주로 나눌 수 있다.

  • 인프라 코드
  • 데이터베이스나 서드파티 시스템과 같은 외부 서비스 및 종속성
  • 모든 것을 하나로 묶는 코드

# 1.4.3 최소한의 유지비로 최대의 가치를 끌어냄

  • 가치 있는 테스트(더 나아가, 가치가 낮은 테스트) 식별하기
  • 가치 있는 테스트 작성하기

# 1.5

동료에게 생각을 명확하게 전달하는 능력은 값으로 매길 수 없다. 소프트웨어 개발자는 그 결정이 내려진 이유를 정확히 설명할 수 없다면 설계 결정에 대해 완전히 인정받지 못한다.

# 2.1 단위 테스트 정의

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

위에서 세번째 속성이 대중에 의견이 크게 갈리는 부분. 격리 문제는 단위 테스트의 고전파와 런던파를 구분할 수 있게 해주는 근원적인 차이에 속한다.

고전파 스타일

  • AAA패턴 (Arrange, Act, Assert)

런던파 스타일

  • 목 사용
  • 목은 테스트 대상 시스템과 협력자 간의 상호 작용을 검사할 수 있는 특별한 테스트 대역.
  • 목은 테스트 대역의 부분 집합
  • 테스트 대역은 실행과 관련 없이 모든 종류의 가짜 의존성을 설명하는 포괄적인 용어. 목은 그러한 의존성의 한 종류이다.

# 2.1.1 격리 문제에 런던파 접근

하나의 클래스 다른 또는 여러 클래스에 의존하면 이 모든 의존성을 테스트 대역으로 대체. 이런 식으로 동작을 외부영향과 분리해서 테스트 대상 클래스에만 집중 가능. 이 방법의 또 다른 이점은 테스트 실패시 어느 부분이 고장 났는지 확실히 알 수 있다는 점. 테스트 대역을 사용해 객체 그래프를 다시 만들지 않아도 됨. 또 다른 이점은 한 번에 한 클래스만 테스트하는 지침을 도입하여 전체 단위 테스트 스위트를 간단한 구조로 할 수 있다.

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

공유 의존성. 단위 테스트끼리 격리.

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

내용에 의해서만 식별됨. 즉, 두 객체가 동일한 내용만 가지고 있다면 어떤 객체를 사용해도 상관 없다. 이러한 인스턴스는 서로 바꿔 사용할 수 있다.

# 2.3 고전파와 런던파의 비교

고전파와 런던파의 주요 차이는 단위 테스트의 정의에서 격리 문제를 어떻게 다루는지에 있다. 결국 테스트해야 할 단위의 처리와 의존성 취급에 대한 방법

# 런던파의 접근 방식의 이점

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

응집도가 높고 의미가 있어야 한다.
예시를 들자면 "우리집 강아지를 부르면, 바로 나에게 온다." 와 "우리집 강아지를 부르면 먼저 왼쪽 앞다리를 움직이고, 이어서 오른쪽 다리를 움직이고, 머리를 돌리고, 꼬리를 흔들기 시작한다..." 두 번째는 안 좋은 사례이다. 저 움직임이 나에게 오는지, 도망가는지 알 수 없다. 대신 개별 클래스(다리, 머리, 꼬리)를 목표로 할 때 테스트가 이렇게 보이기도 한다.

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

  • 테스트 주도 개발(TDD, Test-Driven Development)을 통한 시스템 설계 방식
  • 과도한 명세(over-specification) 문제

런던 스타일의 단위 테스트는 하향식 TDD, 고전적 스타일은 일반적으로 상향식으로 한다. 그러나 고전파와 런던파 간의 가장 중요한 차이점은 과도한 명세문제, 즉 테스트가 SUT의 구현 세부 사항에 결합되는 것이다. 런던 스타일이 테스트가 구현에 더 자주 결합되는 편이다. 이로 인해 런던 스타일과 목을 전반적으로 아무 데나 쓰는 것에 대해 주로 이의가 제기된다.

# 2.4 두 분파의 통합 테스트

고전파의 관점에서 단위 테스트는

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

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

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

# 3.1 단위 테스트를 구성하는 방법

# 3.1.1 AAA 패턴 사용

준비, 실행, 검증 세 부분으로 나눌 수 있다. AAA 패턴은 스위트 내 모든 테스트가 단순하고 균일한 구조를 갖는 데 도움이 된다.

  • 준비 구절에는 테스트 대상 시스템(SUT, System Under Test)과 해당 의존성을 원하는 상태로 만든다.
  • 실행 구절에서는 SUT에서 메서드를 호출하고 준비된 의존성을 전달하며 (출력이 있으면) 출력 값을 캡처한다.
  • 검증 구절에서는 결과를 검증한다. 결과는 반환 값이나 SUT와 협력자의 최종 상태, SUT가 협력자에 호출한 메서드 등으로 표시될 수 있다.

AAA와 유사한 Given-When-Then 패턴

  • Given - 준비 구절에 해당
  • When - 실행 구절에 해당
  • Then - 검증 구절에 해당 테스트 구성 측면에서 AAA와 차이는 없다. 프로그래머가 아닌 사람이 읽기 더 쉽다. 비기술자와 공유하는 테스트에 적합.

# 3.1.3 테스트 내 if 문 피하기

안티 패턴이다. if문은 테스트가 한 번에 너무 많은 것을 검증한다는 표시다. 그러므로 이러한 테스트는 반드시 여러 테스트로 나눠야 함. if문은 테스트를 읽고 이해하는 것을 더 어렵게 만듬.

# 3.3.1 테스트 간의 높은 결합도는 안티 패턴

테스트를 수정해도 다른 테스트에 영향을 주어서는 안된다.

# 3.3.2 테스트 가독성을 떨어뜨리는 생성자 사용

준비 코드를 생성자로 추출할 때 또 다른 단점은 테스트 가독성을 떨어뜨리는 것. 테스트만 보고서는 전체 그림 파악 불가.

# 3.4 단위 테스트 명명법

가장 유명하지만 가장 도움이 되지 않는 방법은 다음과 같음.
테스트 대상 메서드_시나리오_예상 결과

  • 테스트 대상 메서드: 테스트 중인 메서드의 이름
  • 시나리오: 메서드를 테스트하는 조건
  • 예상 결과: 현재 시나리오에서 테스트 대상 메서드에 기대하는 것 동작 대신 구현 세부 사항에 집중하게끔 부추기기 때문에 분명히 도움이 되지 않는다.
public void Sum_of_two_numbers() // O
public void Sum_TowNumbers_ReturnsSum() // X

# 3.4.1 단위 테스트 명명 지침

표현력 있고 읽기 쉬운 테스트 이름을 지으려면 다음 지침을 따르자.

  • 엄격한 명명 정책을 따르지 않는다. 복잡한 동작에 대한 높은 수준의 설정을 이러한 정책의 좁은 상자안에 넣을 수 없다. 표현의 자유를 허용하라.
  • 문제 도메인에 익숙한 비개발자들에게 시나리오를 설명하는 것처럼 테스트 이름을 짓자.
  • 단어를 밑줄(underscore)표시로 구분한다. 긴 이름에 가독성을 올려준다.