Skip to content

ju-learning/book-unit-testing-pub

Repository files navigation

🗂 예제 자료

📝 내용 정리

1부 더 큰 그림

1장 단위 테스트의 목표

  • 단위 테스트를 배우는 것은 테스트 프레임워크나 목 라이브러리(mocking library) 등과 같은 기술을 익히는 것에 그치치 않고, 단순히 테스트를 작성하는 것보다 더 큰 범주이다

1.1 단위 테스트 현황

  • 지난 20년간 단위 테스트를 적용할 것을 독려하는 분위기가 자리잡았으며, 이제 대부분의 회사에서 필수로 간주될 정도로 독려하는데 성공
  • 그냥 쓰고 버리는 프로젝트가 아니라면, 단위 테스트는 늘 적용해야 한다
  • 시장의 논쟁은 이제 “단위테스트를 작성해야 하는가?” 에서 “좋은 단위테스트란 무엇인가?” 로 바뀌었고, 이는 여전히 매우 혼란스러움

1.2 단위 테스트의 목표

  • 단위 테스트와 코드 설계의 관계
    • 코드를 단위테스트 하기 어렵다면 코드 개선이 반드시 필요하다는 것을 의미
    • 보통 강결합(tight coupling) 코드에서 저품질이 나타나는데, 여기서 강결합은 제품 코드가 서로 충분히 분리되지 않아서 따로 테스트하기 어려움을 말함
  • 단위 테스트의 목표는 무엇인가? → 소프트웨어 프로젝트의 지속 가능한 성장을 가능하게 하는것이 핵심 목표

Untitled

  • 처음 개발을 시작할때는 잘못된 아키텍처 결정이 없고, 걱정할만한 코드가 없지만 시간이 점점 지나면서 개발 속도가 현저히 느려지고 심지어 전혀 진행하지 못할 정도로 느려질 수 있다
  • 이렇게 개발 속도가 빠르게 감소하는 현상을 소프트웨어 엔트로피(software entropy) 라고도 한다
  • 지속적인 정리와 리팩터링 등과 같은 적절한 관리를 하지 않고 방치하면 시스템이 점점 더 복잡해지고 무질서해진다
  • 이럴때 테스트는 안전망 역할을 하며, 대부분의 회귀(regression) 에 대한 보험을 제공하는 도구라 할 수 있다
    • 회귀란? 특정 코드 수정 이후에 기능이 의도한 대로 작동하지 않는경우이며, 소프트웨어 버그와 동의어로 사용할 수 있다
  • 한가지 단점은 개발 초기에 테스트코드 작성의 노력이 필요하다는 점
  • 테스트는 지속성과 확장성이 핵심이며, 이를 통해 장기적으로 개발속도를 유지할 수 있음

1.2.1 좋은 테스트와 좋지 않은 테스트를 가르는 요인

  • 단위 테스트가 프로젝트 성장에 도움이 되는 것은 맞지만, 잘못 작성한 테스트는 여전히 프로젝트의 성장을 방해하는 요소
  • 모든 코드에 테스트를 작성할 필요는 없다. 일부 테스트는 아주 중요하고 소프트웨어 품질에 매우 많은 기여를 하지만, 그밖의 테스트는 그렇지 않다
  • 테스트의 가치와 유지비용 요소
    • 기반 코드(비즈니스 코드) 를 리팩터링할 때 테스트도 함께 리팩터링 하라
    • 각 코드 변경 시 테스트를 실행하라
    • 테스트가 잘못된 경고를 발생시킬 경우 처리하라
    • 기반 코드가 어떻게 동작하는지 이해하려고 할 때는 테스트를 읽는데 시간을 투자하라
  • 지속 가능한 프로젝트의 성장을 위해서는 “고품질 테스트”에만 집중해야 함
  • 제품코드(비즈니스) vs 테스트코드
    • 코드가 더 많아질수록, 소프트웨어 내의 잠재적인 버그에 노출되는 표면적이 넓어지고 유지비가 증가함
    • 테스트 코드도 결국 “코드” 이므로 다다익선 이 아님 → 따라서 가능한 적은코드로 확실하게 문제를 해결하는 것이 좋음
    • 테스트도 결국 “애플리케이션의 정확성을 보장하는 것을 목표” 로 하는 문제해결 코드의 일부로 봐야함 → 테스트 역시 버그에 취약하며 유지보수 해야함

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

  • 커버리지 지표: 테스트 스위트가 소스 코드를 얼마나 실행하는지에 대한 백분율
  • 커버리지 지표는 테스트 스위트의 품질을 평가하는데 자주 사용되고 일반적으로 숫자가 높을수록 좋지만, 현실은 그리 간단하지 않음
  • 커버리지 지표는 중요한 피드백을 주더라도 테스트 스위트 품질을 효과적으로 측정하는 데 사용될 수 없다
  • 즉, 커버리지 지표는 괜찮은 부정 지표이지만, 좋지 않은 긍정 지표이다
    • 너무 낮은 커버리지는 “테스트가 충분치 않다” 는 좋은 증거는 맞음
    • 하지만, 커버리지가 높다고해서 “양질의 테스트가 많다”는 증거는 될 수 없음

1.3.1 코드 커버리지 지표에 대한 이해

  • 가장 많이 사용되는 지표는 코드 커버리지(code coverage) 가 있으며, 테스트 커버리지(test coverage) 로도 불린다

Untitled

  • 코드 커버리지는 코드 수가 적을수록 커버리지가 점점 높아지는데, 이는 테스트 스위트나 기반 코드베이스의 유지보수성이 변경되지 않는다
    • ==코드가 적다고 코드가 더 좋아지는게 아니므로

1.3.2 분기 커버리지 지표에 대한 이해

  • 다른 커버리지 지표는 분기 커버리지(branch coverage)
  • 분기 커버리지는 코드 커버리지의 단점을 극복하는데 도움이 되므로, 더 정확한 결과를 제공한다

Untitled

  • 분기 커버리지 지표는 분기 개수만 다루며, 해당 분기를 구현하는데 얼마나 코드가 필요한지 고려하지 않음
  • 같은 if 문을 작성해도 코드커버리지는 if 문 내부의 코드 행 수를, 분기 커버리지는 if 문의 분기 개수(조건 수) 를 판단

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

  • 어떤 커버리지 지표도 의존할 수 없는 이유
    • 테스트 대상 시스템의 모든 가능한 결과를 검증한다고 보장할 수 없음
    • 외부 라이브러리의 코드 경로를 고려할 수 있는 커버리지는 없음
  • 커버리지 지표가 의미가 있으려면 모든 측정 지표(assert문) 을 검증해야 하는데, 모든 대상 코드에 대한 결과를 철저히 검증하는 것이 테스트를 신뢰하거나 좋은 테스트 의 기준이 아님 → 좋은테스트는 “유지보수” 가 생명
  • 모든 커버리지 지표가 테스트 대상 시스템이 메서드를 호출할 때 외부 라이브러리가 통과하는 코드 경로를 고려하지 않음
  • 커버리지 지표로 테스트가 철저하고 테스트가 충분한지는 전혀 알 수 없음

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

  • 예시) 병원 환자의 체온이 높으면 열이 난다는 것을 의미하며 이는 유용한 관찰(==낮은 커버리지), 그러나 병원이 환자의 적절한 체운을 목표로 해서는 안됨(==잘못된 커버리지 목표)
  • 중요) 커버리지 지표는 좋은 부정 지표 이지만, 나쁜 긍정 지표다
  • 단, 시스템 핵심부분은 커버리지를 높게 두는 것이 좋다

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

  • 어떻게 테스트 스위트의 품질을 정확하게 측정할까? → 믿을 만한 방법은 스위트 내 각 테스트를 하나씩 따로 평가하는 것
  • 중요) 성공적인 테스트 스위트의 특성
    • 개발 주기에 통합돼 있다
    • 코드베이스에서 가장 중요한 부분만을 대상으로 한다
    • 최소한의 유지비로 최대의 가치를 끌어낸다

1.4.1 개발 주기에 통합돼 있음

  • 모든 테스트는 개발 주기에 통합돼야 함
  • 이상적으로는 코드가 변경될 때마다 아무리 작은 것이라도 실행해야 함

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

  • 테스트가 주는 가치는 테스트 구조 뿐만 아니라 검증하는 코드에도 있음
  • 대부분의 애플리케이션에서 가장 중요한 부분은 비즈니스로직(도메인 모델) 이 있는 부분
  • 비즈니스 로직 테스트가 시간 투자 대비 최고의 수익을 낼 수 있음
  • 그외의 코드들
    • 인프라 코드
    • 데이터베이스나 서드파티 시스템과 같은 외부 서비스 및 종속성 (e.g. repository, feignclient)
    • 모든 것을 하나로 묶는 코드 (e.g. mapper)
  • 인프라 코드 같은건 중요 알고리즘이 있을 수 있으므로 이럴땐 테스트를 많이 하는것이 좋다
  • 통합 테스트와 같이 일부 테스트는 도메인 모델을 넘어 코드베이스의 중요하지 않은 부분을 포함해 시스템의 전체 동작을 확인할 수 있음
  • 가장 기본은 “도메인 모델” 이며, 도메인 모델의 코드베이스도 중요한 부분과 그렇지 않은 부분을 분리해야 함

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

  • 단위 테스트에서 가장 어려운 부분은 최소 유지비로 최대 가치를 달성하는것이며, 이는 이책에서 말하려는 핵심
  • 유지비를 상회하는 테스트만 스위트에 유지하는 것이 중요
    • 가치 있는 테스트와 가치 없는 테스트 식별하기
    • 가치 있는 테스트 작성하기
  • 가치 있는 테스트를 작성하려면 코드 설계 기술도 알아야 하며 따라서 책 내용의 상당부는 코드 설계를 설명함
  • 단위 테스트와 기반 코드는 서로 얽혀 있으므로 코드베이스에 노력을 많이 기울이지 않으면 가치 있는 테스트를 만들기 힘듦

1.5 이 책을 통해 배우는 것

  • 테스트 스위트 내의 모든 테스트를 분석하는 데 사용할 수 있는 기준틀을 설명 (이것이 기초) → ~4장
  • 단위 테스트 기술과 실천을 살펴봄 → 4~6장
  • 소프트웨어 개발자는 어떤 결정이 내려진 이유를 정확히 설명할 수 없다면 설계 결정에 대해 완전히 인정받지 못한다

요약

  • 코드베이스에 변경이 생길때마다 무질서(엔트로피) 도 증가하며 코드는 점점 더 나빠지는 경향이 있고, 테스트는 이러한 경향을 뒤집을 수 있다
  • 테스트는 안전망 역할을 하며, 대부분의 회귀에 대한 보험을 제공하는 도구
  • 단위 테스트를 작성하는 것이 중요하며, 그중에서도 “좋은 단위테스트” 를 작성하는 것이 중요하다
  • 단위 테스트의 궁극적인 목표는 “소프트웨어 프로젝트가 지속적으로 성장하게 하는 것”
  • 각각의 테스트는 비용과 편익 요소가 있으므로 모든 테스트를 똑같이 작성 할 필요는 없다
    • 테스트 스위트 내에 가치 있는 테스트만 남기고 모두 제거하라
    • 애플리케이션과 테스트 코드는 모두 자산이 아니라 부채다
  • 단위 테스트를 할 수 없는 코드는 품질이 좋지 못함을 뜻하지만, 단위테스트를 할 수 있다고해서 좋은코드라는 증거는 아니다
  • 마찬가지로 커버리지도 낮다는 것은 문제의 징후지만, 높다고해서 테스트 스위트의 품질이 높다는 것은 아니다
  • 분기 커버리지로 테스트 스위트의 완전성에 대해 더 나은 인사이트를 얻을 수 있지만, 테스트 스위트가 충분한지는 여전히 알 수 없다
  • 특정 커버리지 숫자를 부과하면 동기부여가 잘못된 것 → 커버리지를 위한 테스트를 짤 가능성이 있음
  • 시스템의 핵심 부분에 커버리지를 높게 갖는것은 좋지만, 이 높은 수준을 요건으로 삼는것은 좋지 않다
  • 성공적인 테스트 스위트의 조건
    • 개발주기에 통합돼 있음
    • 코드베이스의 핵심 부분을 대상으로 함
    • 최소한의 유지비로 최대한의 가치를 끌어냄
  • 단위 테스트의 목표를 달성하기 위한 유일한 방법
    • 좋은 테스트와 좋지 않은 테스트를 구별하는 방법을 배운다
    • 테스트를 리팩터링하여 더 가치있게 만든다

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

  • 단위 테스트에 접근하는 방법은 두가지 견해로 “고전파(classical school)” 와 “런던파(London school)” 로 나뉘었다
    • 고전파: 모든 사람이 단위 테스트와 테스트 주도 개발에 원론적으로 접근하는 “고전” 방식
    • 런던파: 런던 프로그래밍 커뮤니티에서 시작
  • “단위 테스트” 의 정의가 이 둘을 구분짓는 열쇠
  • 이 책에선 개인적으로 “고전파” 를 선호

2.1 ‘단위 테스트’의 정의

  • (중요하지 않은 것들은 제외한) 단위 테스트의 가장 중요한 세가지 속성
    • 작은 코드 조각(단위라고도 함)을 검증
    • 빠르게 수행
    • 격리된 방식으로 처리하는 자동화된 테스트
  • “빠른 단위 테스트” 란 매우 주관적인 척도이므로, 실행 시간이 충분하다면 테스트가 충분히 빠르다는 뜻
  • 격리 문제는 단위 테스트의 고전파와 런던파를 구분할 수 있게 해주는 근원적 차이
  • 고전파 vs 런던파
    • 고전적 접근법은 Detroit(디트로이트) 라고도 하며, 단위 테스트에 대한 고전주의적 접근법임, 켄트백의 “테스트주도 개발” 책을 추천
    • 런던 스타일은 mockist(목 추종자) 로 불린다

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

  • 런던파에서는 테스트 대상 시스템을 협력자(collaborator) 에게서 격리하는 것을 “격리된 방식” 이라고 말한다
  • 즉, 하나의 클래스가 다른 클래스에 의존하면 이 “모든” 의존성을 테스트 대역(test double) 로 대체해야 대상 클래스에만 집중할 수 있다고 함
  • 이 방법의 이점들
    • 테스트가 실패하면 코드베이스의 어느 부분이 고장 났는지 확실히 알 수 있음 → 다른 의존성을 모두 대체 했기 때문에 테스트가 실패하면 테스트 대상이 문제인 것
    • 객체 그래프(object graph) 를 분할할 수 있음 → 의존성을 가진 코드베이스를 테스트하는 것은 어렵지만, 대역을 사용하면 추가 의존성을 주입하지 않고도 테스트 할 수 있음 (단위 테스트 준비를 크게 줄일 수 있음)
    • 프로젝트 전반적으로 한 번에 한 클래스만 테스트를 할 수 있음 (테스트 스위트 구조를 간단히 할 수 있음) → 각각의 클래스는 각각의 테스트 단위로 테스트 하기 때문

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

  • 런던 스타일은 테스트 대역(목) 으로 테스트 대상 코드 조각을 분리해서 격리요구 사항을 맞춤
  • 단위 테스트에서 “작은 코드 조각을 검증” 한다는 뜻에도 다양한 해석이 가능하지만, 일반적으로 한 번에 한 클래스로 테스트하는 지침을 따르려고 노력해야 함
  • 단위테스트는 서로 격리해서 실행해야 테스트를 어떤 순서든 가장 적합한 방식으로 실행할 수 있으며 서로의 결과에 영향을 미치지 않는다
  • 각각의 테스트를 격리하는 것은 여러 클래스가 모두 메모리에 상주하고 공유 상태에 도달하지 않는 한, 여러 클래스를 한 번에 테스트 해도 괜찮다는 뜻
  • 고전파에선 테스트 간에 공유 상태를 일으키는 의존성에 대해서만 테스트 대역을 사용한다
  • 공유 의존성, 비공개 의존성, 프로세스 외부 의존성 + 공유 의존성과 휘발성 의존성
    • 공유 의존성(shared dependency): 테스트 간에 공유되고 서로의 결괴에 영향을 미칠 수 있는 수단을 제공하는 의존성 (e.g. 정적 가변 필드 (static fields), 데이터베이스)
    • 비공개 의존성(private dependency): 공유하지 않는 의존성
    • 프로세스 외부 의존성(out-of-process dependency): 애플리케이션 실행 프로세스 외부에서 실행되는 의존성이며, 아직 메모리에 없는 데이터에 대한 프록시(proxy, 가짜객체) (e.g. 외부 API, 데이터베이스)
    • 휘발성 의존성은 공유 의존성과 비슷하지만 다음 중 하나를 나타내는 의존성이다
      • 개발자 머신에 기본 설치된 환경 외에 런타임 환경의 설정 및 구성을 요구 (e.g. 데이터베이스는 환경마다 “기본”으로 설치되어있지 않음)
      • 비결정적 동작(nondeterministic behavior) 을 포함한다 (e.g. Math.random() 같은 난수생성기나 현재날짜 now() 처럼 각 호출에 대해 다른 결과를 제공)
    • e.g.
      • 데이터베이스는 “프로세스외부+공유 의존성” 이지만, 각 테스트 실행전 도커 컨테이너로 데이터베이스를 시작하도록 인스턴스를 분리하면 테스트끼리 공유하지 않기 때문에 “프로세스외부+공유하지않는 의존성” 이다
      • 싱글턴(singleton) 의존성은 각 테스트에서 새 인스턴스를 만들 수 있기만 하면 공유되지않고, 인스턴스는 하나지만 테스트는 이 패턴을 따르지 않으므로 “비공개 의존성” 이다
      • 난수 생성기는 휘발성(임시 데이터) 지만, 각 테스트에 별도의 인스턴스를 제공할 수 있으므로 공유 의존성이 아니다
  • 공유 의존성은 테스트 대상 클래스 간이 아니라 단위 테스트 간에 공유한다
  • 공유 의존성을 대체하는 또 다른 이유는 테스트 실행 속도를 높이는 데 있음
    • 데이터베이스나 파일 시스템 등의 공유 의존성에 대한 호출은 비공개 의존성에 대한 호출보다 더 오래 걸림
    • 이러한 호출을 포함하는 공유 의존성을 가진 테스트는 단위테스트에서 통합테스트 영역으로 넘어감
  • 단위가 반드시 클래스에 국한될 필요는 없음, 공유 의존성이 없는 한 여러 클래스를 묶어서 단위 테스트 할 수도 있음

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

  • 단위테스트를 각각 런던파는 “테스트 대상 시스템에서 협력자를 격리하는것”, 고전파는 “단위 테스트끼리 격리하는 것” 으로 본다
  • 정리하면 아래에 대한 의견 차이가 있음
    • 격리 요구 사항
    • 테스트 대상 코드 조각(단위) 의 구성요소
    • 의존성 처리

Untitled

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

  • 테스트 대역을 어디에서나 흔히 사용할 수 있지만, 런던파는 테스트에서 일부 의존성을 그대로 사용할 수 있도록 함
  • 불변 객체(절때 변하지 않는 객체) 는 교체하지 않아도 됨
    • RemoveInventory(Product.Shampoo, 5) 라는 구문에서 ‘5’ 라는 숫자는 변수조차 사용하지 않는다
    • 이러한 불변 객체를 값 객체(value object) 또는 값(value) 라고 하며, 주요 특징은 각각의 정체성이 없고 내용에 의해서만 식별 된다는 것
    • 값 객체는 내용(값)이 동일하다면 무엇을 사용해도 되며, 인스턴스를 서로 바꿔 사용할 수 있다
    • 추가적인 궁금증은 https://enterprisecraftsmanship.com/posts/entity-vs-value-object-the-ultimate-list-of-differences/ 를 참조

Untitled

  • (런던파와 고전파가 단위테스트에서 의존성들을 어떻게 생각하는가에 대한 그림)
  • 모든 공유 의존성은 변경 가능하지만, 변경 가능한 의존성을 공유하려면 여러 테스트에서 재사용돼야 한다
  • 협력자 vs 의존성
    • 협력자(collaborator) 는 공유하거나 변경 가능한 의존성
      • 데이터베이스는 공유 의존성이므로 “데이터베이스 접근 권한” 을 제공하는 협력자다
      • 주입받는 객체 (e.g. Store) 도 시간에 따라 상태가 변할 수 있기 때문에 협력자다
      • 숫자 5 같은 불변객체는 의존성이지만 협력자는 아니다 (값 객체다)
    • 일반적인 클래스는 “협력자” 와 “값” 이라는 두가지 유형의 의존성으로 동작
  • 중요) 모든 프로세스 외부 의존성이 공유 의존성의 범주에 속하는 것이 아님 → 공유 의존성은 거의 프로세스 외부에 있긴 하지만, 반대로 모든 외부 프로세스가 공유 의존성은 아니라는 뜻

Untitled

  • 테스트가 반환하는 데이터에 영향을 미칠 수 없으면 공유가 아니다
  • 대부분의 경우 테스트 속도를 높이려면 테스트 대역으로 교체해야 하지만, 프로세스 외부 의존성이 충분히 빠르고 연결이 안정적이면 테스트에서 그대로 사용해도 괜찮다
  • (책에서 달리 명시하지 않는 한, 공유 의존성과 프로세스 외부 의존성이라는 용어는 같다. 왜냐면 실제 프로젝트에서 프로세스 외부가 아닌 공유의존성의 거의 없기 때문)

2.3 고전파와 런던파의 비교

  • 복습하면 고전파와 런던파 간의 주요 차이는 단위 테스트의 정의에서 격리를 어떻게 다루는지에 있다
  • (개인적으로 저자는 고전파의 단위테스트를 선호 → 목을 사용하는 테스트는 고전적인 테스트보다 지속 가능한 성장을 달성하는데 더 불안정한 경향이 있기 때문)
  • 런던파의 이점
    • 입자성(granularity) 이 좋다 → 테스트가 세밀해서(fine-grained) 한번에 한 클래스씩만 확인하기 때문
    • 서로 연결된 클래스의 그래프가 커져도 테스트 하기 쉽다 → 의존성이 대역으로 대체되기 때문에
    • 테스트가 실패하면 어떤 기능에서 실패한건지 명확히 알 수 있다 → 협력자가 없기때문에 해당 클래스의 기능에서만 오류가 발생함

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

  • 단위 테스트를 런던파는 클래스 단위로 간주한다
  • 객체지향 프로그래밍 경력을 가진 개발자들은 보통 클래스를 모든 코드베이스의 기초에 위치한 원자 빌딩 블록(atomic building block) 으로 간주하므로 자연스럽게 클래스를 “테스트에서 검증할 원자단위” 로 취급하게 한다
  • 팁) 테스트는 코드의 단위로 검증하면 안되고 문제 영역에 의미가 있는(비즈니스 담당자가 유용하다고 인식할 수 있는) 동작의 단위로 검증해야 한다
  • 사실 좋은 코드 입자성을 목표로 하는것은 도움이 되지않고 테스트가 “단일 동작 단위를 검증” 한다면 좋은 테스트다
  • 테스트는 프로그래머가 아닌 일반 사람들에게 응집도가 높고 의미가 있는, “해결하는 데 도움이 되는 문제에 대한 이야기”를 들려줘야 한다
  • 실제 동작 대신 개별 클래스를 목표로 테스트하면, 테스트가 이상하게 보이기 시작한다
    • e.g. 강아지를 부르면 내게 온다 vs 강아지를 부르면 먼저 앞발을 내밀고 뒷발을 움직이고 앞발을 다시 내려놓고 뒷발을 내려놓고 꼬리를 흔들고 …

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

  • 실제 협력자(의존성) 을 대신 목을 사용하면 클래스를 쉽게 테스트 할 수 있다
  • 이것을 고전파로 시행하면 대상 시스템 전체 객체 그래프(의존성들) 을 전부 셋팅해야해서 작업량이 많을 수 있다
  • (이런것들은 사실이지만) 상호 연결된 클래스의 크고 복잡한 그래프를 작성하는 방법을 찾는것보다 이러한 클래스 그래프들이 만들어지지 않도록 설계하는것이 선행되어야 한다
  • 대개 클래스 그래프가 커진 것은 코드 설계 문제의 결과다
  • 코드 조각을 단위테스트 하는 능력은 비교적 높은 정확도로 코드 저품질을 예측함
  • 목을 사용해서 이러한 복잡한 객체 그래프를 쉽게 테스트 하는것은 문제를 감추기만 할 뿐 근본적인 원인을 해결하지는 못한다

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

  • 런던 스타일 테스트가 있는 시스템에 버그가 생기면, 보통 SUT에 버그가 포함한 테스트만 실패한다
  • 고전방식이면 오작동하는 클래스를 참조하는 모든 클래스의 테스트가 실패하게 된다
  • 하나의 버그가 전체 시스템에 걸쳐 테스트 실패를 야기하는 것은 원인을 찾기 어렵지만, 마지막으로 수정한 부분이 알아내는것은 크게 어렵지 않으므로 큰 문제가 되지 않는다
  • 또한, 하나의 버그가 많은 테스트를 실패로 만들었다면 이는 수정한 코드가 전체 시스템에 의존하는 (큰 가치가 있는) 중요한 코드임을 시사하므로 가치가 있다

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

  • 위에서 설명하지 않은 런던파와 고전파 사이의 남아있는 두가지 차이점
    • 테스트 주도 개발(Test-Driven Development; TDD) 를 통한 설계 방식
    • 과도한 명세(over-specification) 문제
  • 테스트주도개발?
    • 테스트 주도 개발은 테스트에 의존해 프로젝트 개발을 추진하는 소프트웨어개발 프로세스
    • 테스트 주도개발 프로세스의 세 단계
      1. 추가해야 할 기능과 어떻게 동작해야 하는지를 나타내는 실패 케이스 작성
      2. 테스트가 통과할 만큼 충분히 코드를 작성 (이 단계에서 코드가 깨끗하거나 명쾌할 필요는 없음)
      3. 통과된 테스트의 보호하에서 코드를 안전하고 읽기 쉽게 리팩터링(유지보수 가능한 형태로 수정)
    • 런던 스타일은 하향식 TDD (전체 시스템에 대한 기대치를 설정하는 상위 레벨 테스트부터 시작)
    • 고전파는 상향식 TDD (도메인 모델을 시작으로 최종 소프트웨어까지 위로) → 의존성이 실제 객체를 다뤄야 하기 때문
  • 런던파와 고전파의 중요한 차이점은 과도한 명세(테스트가 SUT 일부 구현 세부 사항에 결합되는 것) 다
  • 런던 스타일이 고전 스타일보다 테스트가 구현에 더 자주 결합되는 편 → 의존성의 동작을 mock 해야 하기 때문

2.4 두 분파의 통합 테스트

  • 런던파는 실제 협력자 객체를 사용하는 모든 테스트를 통합 테스트로 간주, 따라서 고전 스타일로 작성된 대부분의 테스트는 런던파 입장에선 통합테스트에 가까움
  • (이 책에선 단위 테스트와 통합 테스트의 고전적인 정의를 사용)
  • 단위테스트는 다음과 같은 특징을 가짐 (→ 고전파로 번역)
    • 작은 코드 조각을 검증 → 단일 동작 단위를 검증
    • 빠르게 수행 → 빠르게 수행
    • 격리된 방식으로 처리 → 다른 테스트와 별도로 처리
  • 통합테스트는 바로 위의 단위 테스트 기준 중 하나를 충족하지 않는 테스트임
    • e.g. 공유의존성(데이터베이스 같은) 에 접근하는 테스트는 다른 테스트와 분리해 실행할 수없으므로 통합테스트임
    • 프로세스 외부 의존성에 접근하면 테스트가 느려질 수 있음
  • 둘 이상의 동작 단위를 검증할 때의 테스트는 통합 테스트임
  • 다른 팀이 개발한 모듈이 둘 이상 있을 때 통합 테스트로 어떻게 작동하는지 검증할 수 있음
  • 통합 테스트는 시스템 전체를 검증해 소프트웨어 품질을 기여하는 데 중요한 역할을 함

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

  • 통합 테스트는 공유 의존성, 프로세스 외부 의존성뿐 아니라 조직 내 다른 팀이 개발한 코드 등과 통합해 작동하는지도 검증하는 테스트
  • 코드가 프로세스 외부 종속성과 함께 어떻게 작동하는지 검증하는 [엔드 투 엔드 테스트] 가 있으며 이는 통합 테스트 종류의 하나다
    • 일반적으로 엔드 투 엔드 테스트가 일반적으로 의존성을 더 많이 포함 함
  • 일반적으로 통합테스트는 프로세스 외부 의존성을 한두 개만 갖고 작동하지만, 엔드 투 엔드 테스트는 외부 의존성을 전부 혹은 대부분 가지고 작동
  • “모든 외부 애플리케이션을 포함해 시스템을 최종 사용자의 관점에서 검증하는 것” 을 엔드 투 엔드 테스트로 본다

Untitled

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

요약

  • 단위 테스트의 정의
    • 단일 동작 단위를 검증
    • 빠르게 수행
    • 다른 테스트와 별도로 처리
  • 격리 문제를 주제로 고전파와 런던파 두 분파로 나뉘며, 이러한 의견 차이는 무엇이 단위를 의미하는지에 대한 관점과 SUT 의 의존성 처리 방식에 영향을 미침
    • 런던파는 테스트 대상 단위를 서로 분리하고, 불변 의존성을 제외한 모든 의존성을 테스트 대역으로 대체해야 함
    • 고전파는 단위가 아니라 단위 테스트를 서로 분리해야 한다고 주장 (테스트 대상 단위는 코드 가 아니라 동작 단위임 )
  • 런던파는 더나은 입자성, 연결된 클래스의 큰 그래프에 대한 테스트 용이성, 실패한 버그를 빠르고 쉽게 찾을 수 있는 장점이 있음
  • 다만, 아래 이유들로 인해 런던파의 장점이 희석됨
    • 테스트의 단위는 “코드” 가 아니라 “동작” 임
    • 코드 조각을 단위 테스트 할 수 없다는 것은 코드 설계에 문제가 있다는 사실을 시사함
    • 테스트 실패 후 어디서 어떤 버그가 발생했는지 판단하는것은 크게 어려운 문제가 아님
  • 런던파의 가장 큰 문제는 과잉명세(SUT 가 세부 구현에 결합된 테스트) 임
  • 통합 테스트는 단위 테스트 기준 3가지 중 하나 이상을 충족하지 못하는 테스트이며, 엔드 투 엔드 테스트도 통합 테스트의 일부임
  • 추천 참고 자료
    • 고전 스타일을 더 알고싶다면 켄트백의 책 Test-Driven Development: By Example 을 추천
    • 런던 스타일을 더 알고싶다면 스티브 프리먼과 냇프라이스의 Growing Object-Oriented Software, Guided by Tests 를 참고
    • 의존성 작업을 더 알고싶다면 스티븐 반 듀르센과 마크시먼의 Dependency Injection: Principles, Practices, Patterns 를 추천

3장 단위 테스트 구조

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

3.1.1 AAA 패턴 사용

  • AAA 패턴은 각각 준비(Arrange), 실행(Act), 검증(Assert) 이라는 세 구성요소를 가진 패턴 이다 (3A 패턴이라고도 함)
  • AAA 패턴은 스위트 내 모든 테스트가 균일한 구조를 갖는데 도움을 주며, 이러한 일관성이 이 패턴의 가장 큰 장점중 하나다
  • 구조 상세
    • 준비: 테스트 대상 시스템(SUT) 과 해당 의존성을 원하는 상태로 만듦
    • 실행: SUT 에서 메서드를 호출하고, 준비된 의존성을 전달하며 출력 값을 캡쳐
    • 검증: 실행의 반환값이나 SUT와 협력자의 최종 상태등을 검증
  • Given-When-Then 패턴
    • Given - AAA 패턴의 준비구절에 해당
    • When - AAA 패턴의 실행 구절에 해당
    • Then - AAA 패턴의 검증 구절에 해당
    • 테스트 구성 측면에서 두 가지 패턴 사이에 차이는 없고, 다만 given-when-then 구조가 더 읽기 쉽다
  • 일반적으로 테스트를 작성할 때 준비 구절 부터 시작하는것이 자연스러움
  • 다만, 테스트주도개발(Test-Driven Development; TDD) 를 실천할 때는 검증 구절을 먼저 시작
    • TDD 방법의 경우 기능을 개발하기 전에 실패 테스트를 만들때는 아직 기능이 어떻게 동작할지 알 수 없으므로
    • TDD 가 아닐때는 역시 준비구절로 하는게 좋다

3.1.2 여러 개의 준비, 실행, 검증 구절 피하기

  • 여러개의 준비, 실행, 검증 구절은 테스트가 너무 많은 것을 한번에 검증한다는 의미 → 이러한 테스트는 여러 테스트로 나눠서 해결
  • 실행이 하나면 테스트가 단위 테스트 범주에 있게끔 보장하고, 간단하고, 빠르며 이해하기 쉬움
  • 통합 테스트에서는 실행 구절을 여러개 두는것도 괜찮을 때가 있음
    • 통합테스트는 느릴 수 있는데, 이때 속도를 높이기 위해 여러 개의 통합 테스트를 여러 실행과 검증이 있는 단일 테스트로 묶는것도 방법이 될 수 있음
  • 단위 테스트나 충분히 빠른 테스트에선 이러한 테스트 묶기 최적화가 필요하지 않으며, 항상 다단계 단위 테스트를 여러개로 나누는 것이 좋음

3.1.3 테스트 내 if 문 피하기

  • 준비,실행,검증 구절을 여러차례 사용하는것과 비슷하게, if 문을 사용한 테스트가 있는데 이것 또한 안티패턴이다
  • 단위든 통합이든 “테스트” 는 “분기가 없는 간단한 일련의 단계” 여야함
  • if 문은 테스트가 한번에 너무 많은 것을 검증한다는 표시
  • if 문은 테스트를 읽고 이해하는 것을 어렵게하고 추가 유지비만 불어남 → 테스트에 분기가 있음으로 얻는 이점은 없음

3.1.4 각 구절은 얼마나 커야 하는가?

  • 준비 구절이 가장 큰 경우
    • 일반적으로 준비 구절이 세 구절(준비, 실행, 검증) 중에 가장 큼
    • 그러나 준비 구절이 지나치게 크면 같은 테스트 클래스 내 비공개 메서드 또는 별도의 팩토리 클래스로 도출하는 것이 좋음
    • 준비 구절에서 코드 재사용에 도움이 되는 두가지 패턴으로 “오브젝트 마더(Object Mother) 와 테스트 데이터 빌더(Test Data Builder)” 가 있음
  • 실행 구절이 한줄 이상인 경우를 경계하라
    • 실행 구절은 보통 한줄
    • 실행 구절이 두줄 이상인 경우, SUT의 공개 API 에 문제가 있을 수 있다는 신호이며 두번째 실행로직을 실행하려면 캡슐화가 깨지게 된다
    • 실행 구절의 첫번째 메서드 호출과 그 이후의 메서드호출이 있다면, 이후의 메서드 호출을 하지 않을 시 모순이 발생하고 이러한 모순을 불변위반(Invariant violation) 이라 한다
    • 잠재적 모순으로 부터 코드를 보호하는 행위를 캡슐화 (encapsulation) 이라 하고, 데이터베이스에 모순이 생기면 큰 문제가 된다 → 해결책은 캡슐화를 항상 지키는 것
    • 실행 구절을 한줄로 하는 지침은 비즈니스 로직을 포함하는 대부분 코드에 적용되지만, 유틸리티나 인프라 코드는 덜 적용됨

3.1.5 검증 구절에는 검증문이 얼마나 있어야 하는가

  • 가능한 한 가장 작은 코드를 목표로 하는 전제에 세상에는 “테스트당 하나의 검증을 갖는 지침” 이 있지만 단위 테스트의 단위는 동작의 단위이지 코드의 단위가 아니므로 이는 틀렸다
  • 하나의 테스트로 동작의 모든 결과를 평가하는 것이 좋음
  • 단, 검증 구절이 너무 커지면 제품 코드에서 추상화가 누락됐다는 증거가 될 수 있으므로, SUT에서 반환된 객체 내에서 모든 속성을 검증하지 말고 클래스 내에 적절한 동등 멤버(equality member) (equals 메서드 같은거) 를 정의하는 것이 좋다

3.1.6 종료 단계는 어떤가

  • AAA 패턴 이후에 4번째 구절로 (테스트에 의해 작성된 파일을 지우거나 데이터베이스 연결을 종료하는 등의) 종료 구절을 구분하기도 함
  • 하지만 단위테스트는 프로세스 외부에 종속적이지 않으므로 처리해야 할 사이드 이펙트를 남기지 않기때문에, 대부분의 종료 구절이 필요 없다

3.1.7 테스트 대상 시스템 구별하기

  • 기능 동작은 여러 클래스에 걸쳐있을 만큼 클수도, 단일 메서드로 작을 수도 있지만 그 진입지점은 오직 하나만 존재할 수 있다
  • 따라서 SUT의 의존성과 구분하는 것이 중요하고, SUT 가 꽤 많아서 코드에서 대상을 구분히가 힘든 경우, 변수명을 sut 로 하는것이 도움이 될 수 있다

3.1.8 준비, 실행, 검증 주석 제거하기

  • 각각 준비, 실행, 검증 블록들을 빈 줄로 구분하면 대부분의 단위테스트에서 효과적이면서 간결성과 가독성 사이의 균형을 잡을 수 있음
    • AAA 패턴을 따르고 준비 및 검증 구절에 빈 줄을 추가하지 않아도 되는 테스트라면 구절 주석들(e.g. //준비, //검증 ) 을 제거하라
    • 그렇지 않을경우 구절 주석을 유지

3.2 xUnit 테스트 프레임워크 살펴보기

  • (책에서는 C# 이지만, Java 는 JUnit)
  • xUnit 을 선호하는 이유는 대부분 다른 프레임워크보다 깨끗하고 간결하기 때문
  • “각 테스트에는 이야기가 있어야 한다” 는 점을 강조하는 어노테이션들을 xUnit 이 보유하고있고, 이는 문제 영역에 대한 개별적이고 원자적(atomic) 인 사실이나 시나리오이며, 테스트가 통과하는것은 이 사실 또는 시나리오가 실제 사실이라는 증거
  • 테스트가 제품 코드의 기능을 무조건 나열하는것이 아니라 이런 스토리를 가지도록 사고하는게 좋다 (이 명세는 프로그래머뿐만 아니라 비즈니스 담당자에게도 의미가 있어야 함)

3.3 테스트 간 테스트 픽스처 재사용

  • 테스트 픽스처
    • 테스트 픽스처는 테스트 실행 대상 객체다
    • 픽스처 객체는 각 테스트 실행 전에 알려진 고전 상태로 유지하기 때문에 동일한 결과를 생성하며, 따라서 이름이 픽스처라고 불린다
    • (NUnit 에서 TestFixture 테스트가 포함된 클래스를 표시하는 특성으로 불리기도 함)
  • 테스트에서 언제 어떻게 코드를 재사용하는지 아는것이 중요하고, 이러한 준비는 별도의 메서드나 클래스로 도출한 후 테스트간에 재사용하는 것이 좋다
  • 올바르지 않은 재사용 방법
    • 테스트 생성자 (e.g. SetUp 영역)에서 픽스처를 초기화하는 것
    • 준비 구절이 동일할 경우 SetUp 영역 (JUnit 의 @Before 같은 동작) 에서 초기화 할 수도 있지만 다음과 같은 단점이 있음
      • 테스트 간 결합도가 높아짐
      • 테스트 가독성이 떨어짐

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

  • setup 로직에서의 공통 테스트의 준비 로직을 수정하면 모든 테스트에 영향을 미친다
// 예시
...
@BeforeEach
void setup() {
    id = 100L;
}
...
  • 해당 예시에서 id100L 이 아닌 다른 값으로 수정하면 이유없이 다른 테스트들도 다 실패하게 됨
  • 이는 테스트의 중요한 지침인 “테스트를 수정해도 다른 테스트에 영향을 주어서는 안된다” 는 지침을 위반함 → 이 지침을 잘 따르려면 테스트 클래스에 공유 상태를 두지 말아야 함

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

  • 준비 코드를 생성자로 추출할 때의 또다른 단점은 테스트만 보고는 전체 그림을 볼 수 없다. 즉, 테스트 가독성을 떨어뜨리는 것
  • 이러한 방법에서 테스트 메서드가 무엇을 하는지 이해하려면 다른 코드도 다 확인해야하고, 독립적인 테스트는 이러한 불확실성을 두지 않는다

3.3.3 더 나은 테스트 픽스처 재사용법

  • 다른 방법은 테스트 클래스에 비공개 팩토리 메서드(private factory method) 를 두는것
// 예시
public void test1() {
	User user = CreateUserWithTeam("name", 10);
}

public void test2() {
	User user = CreateUserWithTeam("name2", 10);
}

public void CreateUserWithTeam(String userName, Int teamNumber){
	...
}
  • 이렇게 공통 초기화 코드를 비공개 팩토리 메서드로 추출해 테스트 코드를 잛게 하면서 동시에 테스트 진행 상황에 대한 전체 맥락을 유지할 수 있음
  • 비공개 메서드를 충분히 일반화 한다면 테스트가 서로 결합되지 않기때문에 테스트에 픽스처를 어떻게 생성할지 스스로 지정 할 수 있음
  • 테스트 픽스처 재사용 규칙의 한가지 예외는 테스트 전부 또는 대부분에 사용되는 생성자에 픽스처를 인스턴스화 할 수 있음 (e.g. DB와 작동하는 통합테스트)
  • 디비와 작동하는 모든 통합테스트는 데이터베이스 연결이 필요하며, 이 연결을 한 번 초기화한 다음 어디서든 재사용해도 됨
  • 하지만 이또한 기초 클래스(base class) 를 둬서 개별 테스트 클래스가 아니라 클래스 생성자에서 데이터베이스 연결을 초기화 하는것이 더 합리적임 (아래 예시 참고)
// 베이스 클래스 예시 
public static class BaseClass {
	Database _database; 

	public BaseClass() {
		_database = new Database(); 
	}
}

public class TargetTests extends BaseClass {
	...
}

3.4 단위 테스트 명명법

  • 올바른 테스트 명칭은 테스트가 검증하는 내용과 기본 시스템의 동작을 이해하는 데 도움이 되므로 표현력 있는 이름을 붙이는 것이 중요
  • 널리 알려진 명명법 중 [테스트 대상 메서드]_[시나리오]_[예상 결과] 형식으로 구현 세부사항에 집중하게끔 부추기는 명명법은 사실 도움이 되지 않는다
  • 간단하고 쉬운 영어 구문이 훨씬 더 효과적이며 엄격한 명명 구조에 얽매이지 않고 표현력이 뛰어남
  • 결국 단위 테스트는 도메인 전문가가 아니라 프로그래머를 위해 프로그래머가 작성한다
  • public void Sum_of_two_numbers() 같이 쉬운 영어로 작성한 이름이 읽기에 훨씬 간결하고, 테스트 대상 동작에 대한 현실적인 설명이다

3.4.1 단위 테스트 명명 지침

  • 표현력 있고 읽기 쉬운 테스트 이름을 위한 지침
    • 엄격한 명명 정책을 따르지 않으며, 표현의 자유를 허용하자
    • 문제 도메인에 익숙한 비개발자들에게 시나리오를 설명하는 것처럼 테스트 이름을 짓자
    • 단어를 밑줄(underscore) _ 로 구분하면 긴 이름에서 가독성을 향상시킬 수 있다
  • 클래스 명을 지정할 땐 XXXNameTests 패턴을 사용 하지만 테스트가 해당 클래스만 검증하는 것으로 제한하는 것이 아닌, 단위 테스트에서 단위는 동작의 단위이고 클래스 단위가 아님을 명심할 것
  • 팁) XXXNameTest 패턴의 테스트 클래스는 동작 단위로 검증할 수 있는 진입점 또는 API 로 여기자

3.4.2 예제: 지침에 따른 테스트 이름 변경

  • 테스트명 내 테스트 대상 메서드
    • 테스트 이름에 SUT 의 메서드 이름을 포함하지 말라
    • 코드를 테스트 하는 것이 아니라, 애플리케이션 동작을 테스트 하는 것을 명심
    • SUT는 단지 동작을 호출하는 수단(진입점) 일 뿐
    • 동작 대신 코드를 목표로 하면 해당 코드의 구현 세부 사항과 테스트 간의 결합도가 높아지며, 이는 테스트 스위트의 유지보수성에 부정적인 영향을 미친다
    • 예외) 유틸리티 코드를 작업할때는 비즈니스 로직이 없고, 비즈니스 담당자에게는 아무런 의미가 없는 코드이므로 SUT 메서드 이름을 포함해도 됨
  • public void IsDeliveryValid_InvalidDate_ReturnsFalse()
    • 초기 네이밍, [테스트 대상 메서드]_[시나리오]_[예상 결과] 패턴을 따르는 엄격한 명명규칙
  • public void Delivery_with_invalid_date_should_be_considered_invalid()
    • 이름이 프로그래머가 아닌 사람들에도 납득되며, 쉽게 이해할 수 있음
    • SUT의 메서드 이름(IsDeliveryValid) 이 테스트 ㅁ여에 포함되지 않음 → 중요
  • public void Delivery_with_past_date_should_be_considered_invalid()
    • 날짜가 invalid 하다는건, 미래의 유효한 날짜이기 때문에 이러한 맥락을 메서드 명에 적용
    • 하지만, 이름이 너무 장황함
    • 또한, 사실을 서술할 때는 소망이나 욕구가 들어가지 않으므로 should_be 문구는 일반적으로 네이밍 안티패턴이다
    • 영어 메서드명에선 영문법을 지키는 것이 도움이 됨
  • public void Delivery_with_a_past_date_is_invalid()
    • 이게 적당히 잘 지은 메서드명

3.5 매개변수화된 테스트 리팩터링하기

  • 동작이 충분히 복잡하면 이를 설명하는 데 테스트 수가 급격히 증가할 수 있으며 관리가 어려워질 수 있으나, 다행히 대부분의 단위 테스트 프레임워크는 매개변수화된 테스트(parameterized test) 를 사용해 유사한 테스트를 묶을 수 있음

Untitled

  • 매개변수화된 테스트를 사용하면 테스트 코드의 양을 크게 줄일 수 있지만 테스트 메서드가 나타내는 사실을 바로 파악하기 어려워지고, 이는 매개변수가 많을수록 더 어려워짐
  • 긍정적인 테스트케이스는 고유한 테스트로 도출하고 가장 중요한 부분을 잘 설명하는 이름을 쓰면 좀 나아짐
  • 테스트 코드의 양과 그 코드의 가독성은 서로 상충됨
  • 그리고 동작이 너무 복잡하면 매개변수화된 테스트를 조금도 사용하지 말라

3.5.1 매개변수화된 테스트를 위한 데이터 생성

  • xUnit 에는 시용자가 정의 데이터를 생성하는 데 사용할 수 있는 기능이 있고, 이 기능을 사용하면 컴파일러의 제한을 극복하고 모든 유형의 매개변수를 사용 가능

3.6 검증문 라이브러리를 사용한 테스트 가독성 향상

  • 검증문 라이브러리를 사용하면 테스트 가독성을 더욱 높일 수 있음 (Assert.Equal() 대신 result.Should().Be() 처럼 쓸 수 있음)
  • 팁) 객체지향프로그래밍(Object-Oriented Programming; OOP) 패러다임은 이야기처럼 읽을 수 있는 방식으로 코드를 구성 할 수 있기에 가독성 이점을 잘 얻어갈 수 있음
  • 유일한 단점은 프로젝트에 라이브러리를 추가적으로 사용해야 한다는 것

요약

  • 모든 단위 테스트는 AAA 패턴(준비, 실행, 검증) 을 따라야 한다
  • 테스트 내 준비나 실행 또는 검증 구절이 여러개 있으면 테스트가 여러 동작 단위를 한번에 검증한다는뜻이며, 이가 단위 테스트라면 각 동작에 하나씩 여러개의 테스트로 쪼개야 한다
  • 실행 구절이 한 줄 이상이면 SUT의 API에 문제가 있다는 증거다
    • 클라이언트가 항상 실행구절 여러개를 같이 수행해야하고, 이는 잠재적 모순으로 이어질 수 있는데 이것을 “불변 위반”이라고 한다
    • 불변 위반으로부터 코드를 보호하는 것을 캡슐화 라고 함
  • SUT의 이름을 sut 로 지정하여 SUT를 테스트에서 쉽게 구별하도록 하자
  • 테스트 픽스처 초기화 코드는 생성자에 두지말고 팩토리 메서드를 도입해서 재사용 하자
    • 이러한 재사용은 테스트 간 결합도를 상당히 낮게 유지하고 가독성을 향상시켜줌
  • 엄격한 테스트 명명 정책을 지양 하자
  • 매개변수화된 테스트로 유사한 테스트에 필요한 코드의 양을 줄일 수 있고, 단점은 테스트 이름을 더 포괄적으로 만들수록 테스트 이름을 읽기 어렵게 되는 것
  • 검증문 라이브러리를 사용하면 검증문에서 단어 순서를 재구성해 테스트 가독성을 더 향상시킬 수 있다

2부 개발자에게 도움이 되는 테스트 만들기

4장 좋은 단위테스트의 4대 요소

  • (복습) 좋은 단위 테스트 스위트의 특성
    • 개발 주기에 통합되어있음. (실제로 사용하는 테스트만 가치가 있음)
    • 코드베이스의 가장 중요한 부분만을 대상으로 함
    • 최소 유지비로 최대한의 가치 창출

4.1 좋은 단위 테스트의 4대 요소 자세히 살펴보기

  • 좋은 단위 테스트의 네가지 특성 = 회귀방지, 리팩터링내성, 빠른 피드백, 유지보수성

4.1.1 첫 번째 요소: 회귀 방지

  • 회귀방지 == 소프트웨어 버그
  • 코드를 수정한 후 기능이 의도한대로 동작하지 않는 경우를 말함
  • 코드는 자산보다 책임에 가까움 → 코드베이스가 커질수록 잠재적인 버그에 더 많이 노출
  • 회귀 방지 지표에 대한 테스트 점수 평가 고려사항
    • 테스트 중에 실행되는 코드의 양
    • 코드 복잡도
    • 코드의 도메인 유의성
  • 일반적으로, 실행되는 코드가 많을수록 테스트에서 회귀 발생 가능성이 커진다
  • 복잡한 비즈니스 로직을 나타내는 코드가 보일러플레이트 코드보다 중요함 → 비즈니스 로직 코드는 버그 발생시 서비스에 큰 타격을 입기 때문
  • 우리가 작성하지 않은 코드 (라이브러리, 프레임워크, 외부시스템 등) 도 우리가 작성한 코드 만큼이나 소프트웨어 작동에 영향을 미치므로 중요하다
  • 팁) 회귀 방지 지표를 극대화하려면 테스트가 가능한 많은 코드를 실행하는것을 목표로 해야한다

4.1.2 두 번째 요소: 리팩터링 내성

  • 리팩터링 내성: 테스트를 “실패”로 바꾸지 않고 기본 애플리케이션 코드를 리팩터링 할 수 있는지에 대한 척도
  • 거짓 양성: 실제로 기능이 의도한 대로 작동하지만, 테스트는 실패로 뜨는 경우
  • 리팩터링 내성 지표에 대한 테스트 점수 평가 고려사항
    • 테스트에서 얼마나 “거짓 양성” 이 발생하는지 살펴봄 (적을수록 좋다)
  • 단위테스트 목표는 프로젝트 성장을 지속 가능하게 하는 것
  • 테스트가 지속 가능한 성장을 이뤄내는 원리: 회귀 없이 주기적으로 리팩터링하고 새로운기능을 추가할 수 있는 것
    • 기능이 고장났을 때 테스트가 조기 경고를 제공하므로
    • 코드의 변경이 (테스트로 보호로 인해) 회귀로 이어지지 않을 것이란 확신을 줌
  • 거짓양성의 단점
    • 테스트가 타당한 이유 없이 실패하면, 코드 문제에 대응하는 능력과 의지가 희석됨
    • 더이상 테스트를 신뢰할 수 없게됨

4.1.3 무엇이 거짓 양성의 원인인가?

  • 테스트와 테스트 대상 시스템(SUT)의 구현 세부 사항이 많이 결합할수록 허위경보가 많이 발생
  • 거짓 양성 발생 가능성을 줄이는법 == 구현 세부사항에서 테스트를 분리하는 것
    • 테스트를 통해 SUT 가 제공하는 최종결과를 검증하는지 확인할 것
    • 테스트는 최종 사용자 관점에서 SUT를 검증하고 사용자에게 의미있는 결과만 확인
  • 최종 결과가 바뀌지 않더라도 테스트가 실패하는 경우가 있음 → 테스트가 SUT가 생성한 결과가 아니라 SUT의 구현 세부사항과 결합했기 때문

20230707_121840.jpg

  • 리팩터링 과정은 애플리케이션이 식별할 수 있는 동작에 영향을 주지 않으면서 구현을 변경하는 것
  • SUT 구현 세부사항과 결합된 테스트는 리팩터링 내성이 없다 → 이러한 테스트는 리팩터링에 대한 능력과 의지를 방해

4.1.4 구현 세부 사항 대신 최종 결과를 목표로 하기

  • 테스트를 깨지지 않게 하고 리팩터링 내성을 높이는 방법은 SUT의 구현 세부사항과 테스트 간의 결합도를 낮추는 것 뿐
  • 즉, 코드의 내부 작업과 테스트 사이를 가능한 멀리 떨어뜨리고 최종 결과를 목표로 하는 것

20230707_122715.jpg

  • SUT 에서 호출하는 메서드의 매개변수가 변경되어 컴파일 오류가 발생하는 알림도 거짓 양성으로 간주한다

4.2. 첫 번째 특성과 두 번째 특성 간의 본질적인 관계

  • 좋은 단위 테스트의 처음 두 요소(리팩터링 내성과, 회귀방지) 는 본질적으로 정반대 관점에서도 테스트 스위트 정확도에 기여한다
  • 프로젝트가 시작된 직후에는 회귀방지가 중요하지만, 리팩터링 내성은 바로 필요하지 않다

4.2.1. 테스트 정확도 극대화

20230707_123214.jpg

  • 오류 유형
    • 참 음성: 테스트가 통과하고 기본 기능이 의도한 대로 잘 작동하는 상황
    • 참 양성: 기능이 고장 나서 테스트가 실패
    • 거짓 음성: 기능이 고장났지만 테스트에서 오류가 발생하지 않음 → 회귀방지가 도움이 됨
    • 거짓 양성: 기능은 올바르지만 테스트가 여전히 실패로 표시되는 상황 → 리팩터링 내성이 도움이 됨
  • 회귀 방지와 리팩터링 내성은 테스트 스위트 정확도를 극대화하는 것을 목표로 한다
    • 테스트가 버그 있음을 얼마나 잘 나타내는가(거짓 음성 제외)
    • 테스트가 버그 없음을 얼마나 잘 나타내는가(거짓 양성 제외)

20230707_123940.jpg

  • 테스트 정확도를 향상시키는 방법은 두가지 (둘다 매우 중요함)
    • 분자(신호) 를 증가시키는 것 → 버그 더 잘 찾아줌
    • 분모(소음) 을 줄이는 것 → 엉뚱한 알람을 줄여줌

4.2.2 거짓 양성과 거짓 음성의 중요성: 역학 관계

  • 단기적으로는 거짓 양성도 거짓 음성만큼 나쁘지 않다. 그러나 프로젝트가 성장함에 따라 거짓 양성은 테스트 스위트에 점점더 큰 영향을 미침

20230707_124433.jpg

  • 시간이 흐를수록 코드베이스는 더 나빠짐 + 점점 복잡해지고 체계적이지 않게 된다 → 이러한 경향을 줄이려면 주기적으로 리팩터링 해야 함
  • 리팩터링이 점점 더 필요해짐에 따라 테스트에서 리팩터링 내성도 점점 더 중요해짐
  • 대부분 좋은 단위테스트의 첫번째 특성인 “회귀방지” 에만 중점을 두는 경향이 있다
  • 그러나 회귀방지는 “프로젝트의 성장을 유지하는데 도움이 되고, 가치가 있으며, 매우 정확한 테스트 스위트를 구축”하기엔 모자르다
  • 중대형 프로젝트에서 작업하면 “거짓 음성” 과 “거짓 양성” 에 대해 똑같이 주의를 기울여야 한다

4.3. 세 번째 요소와 네 번째 요소: 빠른 피드백과 유지보수성

  • 좋은 단위테스트의 (회귀방지, 리팩터링내성 을 제외한) 남은 두가지 특성
    • 빠른 피드백
    • 유지보수성
  • 빠른 피드백은 단위테스트의 필수속성이며, 테스트 속도가 빠를수록 테스트 스위트에서 더 많은 테스트를 자주 수행할 수 있다
  • 유지보수성 지표는 다음 두가지 요소로 유지비를 평가한다
    • 테스트가 얼마나 이해하기 어려운가
      • 테스트는 코드 라인이 적을수록 더 읽기 쉽다
      • 테스트 코드의 품질은 제품 코드만큼 중요하다
      • 테스트를 작성할 때 절차를 생략하지 말라
      • 테스트 코드를 일급 시민으로 취급하라
    • 테스트가 얼마나 실행하기 어려운가
      • 테스트가 프로세스 외부 종속성으로 작동하면, 의존성을 상시 운영하는데 시간을 들여야 한다

4.4 이상적인 테스트를 찾아서

  • 좋은 단위테스트의 4대 특성(회귀방지, 리팩터링내성, 빠른피드백, 유지보수성)을 곱하면 테스트의 가치가 결정된다 (즉, 하나라도 0이면 가치가 0)
  • 테스트 코드를 포함한 모든 코드는 책임이다
  • 최소 필수값에 대해 상당히 높은 임계치를 설정하고 이 임계치를 충족하는 테스트만 테스트 스위트에 남겨라
  • 소수의 매우 가치있는 테스트가 다수의 평범한 테스트보다 프로젝트가 계속 성장하는데 훨씬 더 효과적이다

4.4.1 이상적인 테스트를 만들 수 있는가?

  • 이상적인 테스트는 네 가지 특성 모두에서 최대점수(1점) 을 받는 테스트지만, 안타깝게도 그런 이상적인 테스트를 만들기는 불가능하다
  • 왜냐하면, 처음 3가지 특성(회귀방지, 리팩터링내성, 빠른피드백)은 서로 상호 배타적이기 때문 (==뭐가 늘어나면 반대로 뭐가 줄어듦)
    • 이 3가지 특성 중 하나를 희생해야 나머지 둘을 최대로 할 수 있다
  • 네가지 테스트 특성 중 하나라도 0점을 받는 테스트는 가치가 없다. 따라서, 특성 중 어느 것도 크게 줄지 않는 방식으로 최대한 크게 해야 한다
  • 아래는 두 특성을 최대로 하는 것을 목표로 하기위해 나머지 한가지 특성을 최대로 희생(0점)해 가치가 0에 가까워진 극단적인 테스트이다

4.4.2 극단적인 사례 1: 엔드 투 엔드 테스트

  • 엔드 투 엔드 테스트는 최종 사용자의 관섬에서 시스템을 살펴보며, 일반적으로 UI, 데이터베이스, 외부 애플리케이션을 포함한 모든 구성요소를 거치게 된다
  • 특징
    • 회귀방지를 훌륭하게 해낸다 → 테스트 하는 코드의 양이 많으므로 대부분의 오류를 발견
    • 리팩터링 내성이 훌륭함 → 실제 사용자 관점에서 기능이 어떻게 동작하는지 검증하므로 구현과 결합되어있지 않음
    • 속도가 느림 → 많은 양의 코드를 검증하고 실제로 데이터베이스나 외부 애플리케이션과 연동되어있어서 실행 속도가 매우 느림

4.4.3 극단적인 사례 2: 간단한 테스트

  • 우수한 리팩터링 내성과 빠른 속도를 보장하지만, 그만큼 문제를 잘 잡아내지 못하므로 회귀방지가 떨어짐

4.4.4 극단적인 사례 3: 깨지기 쉬운 테스트

  • 실행이 빠르고 회귀를 잡을 가능성이 높지만 거짓양성이 많은 테스트(리팩터링 내성이 안좋은) 를 작성하기 매우쉽고, 이를 “깨지기 쉬운 테스트”라 한다
  • 깨지기 쉬운 테스트는 무엇보다 “어떻게”에 중점을 두고 있기 때문에 더이상의 리팩터링을 막고 SUT 구현 세부사항에 스며듦 → “어떻게” 아 아닌 “무엇을” 테스트하는지가 중요

20230710_124602.jpg

4.4.5 이상적인 테스트를 찾아서: 결론

  • 좋은 단위테스트의 세가지 특성(리팩터링 내성, 회귀방지, 빠른피드백) 은 상호 배타적이다
  • 안타깝게도 세가지 모두를 만족시키는 이상적인 테스트를 만들기는 불가능하다
  • 마지막 특성인 “유지보수성” 은 엔드투엔드 테스트만 조금 더 높고(외부의존성 설정을 해야하기때문에), 나머지 테스트들은 비슷비슷하다
  • (이런 결론을 보고있으면 셋을 정당히 포기하고 맞추는것을 생각하겠지만) “리팩터링내성”은 적당히로 포기할 수 없다
    • 리팩터링 내성은 “적당히” 가 아니라 보통 0/1. 즉, 아예 없거나 있거나 둘중하나의 선택이기때문
    • 따라서 리팩터링 내성을 최대로 챙기고, 회귀방지(오류를 더 잘 찾을것인가), 빠른피드백(더 빠르게 실행할것인가) 에서 고민해야 한다
  • 팁) 테스트 스위트를 단단하게 만들려면, 테스트가 불안한 것 (거짓양성) 을 제거하는것이 최우선 과제
  • CAP 정리
    • 좋은 단위테스트의 처음 세가지 특성간의 상충관계는 CAP(CAP theorem) 과 유사
    • Consistency(일관성): 모든 읽기가 가장 최근의 쓰기 또는 오류를 수신하는것
    • Availability(가용성): 모든 요청이 응답을 수신하는 것
    • Partition tolerance(분할 내성): 네트워크 분할에도 시스템이 계속 동작하는 것
    • CAP 도 마찬가지로 이 셋중에 두가지를 선택하는 절충안을 선택한다
    • 마찬가지로 대규모 분산시스템의 “분할내성”은 타협할 수 없다

4.5 대중적인 테스트 자동화 개념 살펴보기

4.5.1 테스트 피라미드 분해

  • 테스트 피라미드는 테스트 스위트에서 테스트 유형간의 비율을 일컫는 개념

20230710_125901.jpg

  • 테스트 피라미드는 종종 세가지유형 (엔드투엔드테스트, 통합테스트, 단위테스트) 로 표현
  • 각 층의 넓이는 테스트가 얼마나 많은가(==테스트의수) 즉, 해당 테스트가 얼마나 보편적인지 나타낸다
  • 층의 높이는 테스트가 얼마나 최종사용자의 동작을 얼마나 유사하게 흉내내는지 나타낸다
  • 피라미드 내 유형에따라 빠른피드백vs회귀방지 사이에서 선택한다

20230710_130345.jpg

5장 목과 테스트 취약성

6장 단위 테스트 스타일

7장 가치 있는 단위 테스트를 위한 리팩터링


3부 통합 테스트

8장 통합 테스트를 하는 이유

9장 목 처리에 대한 모범 사례

10장 데이터베이스 테스트


4부 단위 테스트 안티 패턴

11장 단위 테스트 안티 패턴

📋 메모

🔠 찾아보기

About

책 | 단위 테스트 | 블라디비르 코리코프

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages