- 매닝출판사 도서정보 페이지
- 에이콘출판사 깃허브
- 기타 온라인 자료
- 저자 블로그: https://enterprisecraftsmanship.com/
- 단위테스트 온라인 과정: https://unittestingcourse.com/
1부 더 큰 그림
- 단위 테스트를 배우는 것은 테스트 프레임워크나 목 라이브러리(mocking library) 등과 같은 기술을 익히는 것에 그치치 않고, 단순히 테스트를 작성하는 것보다 더 큰 범주이다
- 지난 20년간 단위 테스트를 적용할 것을 독려하는 분위기가 자리잡았으며, 이제 대부분의 회사에서 필수로 간주될 정도로 독려하는데 성공
- 그냥 쓰고 버리는 프로젝트가 아니라면, 단위 테스트는 늘 적용해야 한다
- 시장의 논쟁은 이제 “단위테스트를 작성해야 하는가?” 에서 “좋은 단위테스트란 무엇인가?” 로 바뀌었고, 이는 여전히 매우 혼란스러움
- 단위 테스트와 코드 설계의 관계
- 코드를 단위테스트 하기 어렵다면 코드 개선이 반드시 필요하다는 것을 의미
- 보통 강결합(tight coupling) 코드에서 저품질이 나타나는데, 여기서 강결합은 제품 코드가 서로 충분히 분리되지 않아서 따로 테스트하기 어려움을 말함
- 단위 테스트의 목표는 무엇인가? → 소프트웨어 프로젝트의 지속 가능한 성장을 가능하게 하는것이 핵심 목표
- 처음 개발을 시작할때는 잘못된 아키텍처 결정이 없고, 걱정할만한 코드가 없지만 시간이 점점 지나면서 개발 속도가 현저히 느려지고 심지어 전혀 진행하지 못할 정도로 느려질 수 있다
- 이렇게 개발 속도가 빠르게 감소하는 현상을 소프트웨어 엔트로피(software entropy) 라고도 한다
- 지속적인 정리와 리팩터링 등과 같은 적절한 관리를 하지 않고 방치하면 시스템이 점점 더 복잡해지고 무질서해진다
- 이럴때 테스트는 안전망 역할을 하며, 대부분의 회귀(regression) 에 대한 보험을 제공하는 도구라 할 수 있다
- 회귀란? 특정 코드 수정 이후에 기능이 의도한 대로 작동하지 않는경우이며, 소프트웨어 버그와 동의어로 사용할 수 있다
- 한가지 단점은 개발 초기에 테스트코드 작성의 노력이 필요하다는 점
- 테스트는 지속성과 확장성이 핵심이며, 이를 통해 장기적으로 개발속도를 유지할 수 있음
- 단위 테스트가 프로젝트 성장에 도움이 되는 것은 맞지만, 잘못 작성한 테스트는 여전히 프로젝트의 성장을 방해하는 요소
- 모든 코드에 테스트를 작성할 필요는 없다. 일부 테스트는 아주 중요하고 소프트웨어 품질에 매우 많은 기여를 하지만, 그밖의 테스트는 그렇지 않다
- 테스트의 가치와 유지비용 요소
- 기반 코드(비즈니스 코드) 를 리팩터링할 때 테스트도 함께 리팩터링 하라
- 각 코드 변경 시 테스트를 실행하라
- 테스트가 잘못된 경고를 발생시킬 경우 처리하라
- 기반 코드가 어떻게 동작하는지 이해하려고 할 때는 테스트를 읽는데 시간을 투자하라
- 지속 가능한 프로젝트의 성장을 위해서는 “고품질 테스트”에만 집중해야 함
- 제품코드(비즈니스) vs 테스트코드
- 코드가 더 많아질수록, 소프트웨어 내의 잠재적인 버그에 노출되는 표면적이 넓어지고 유지비가 증가함
- 테스트 코드도 결국 “코드” 이므로 다다익선 이 아님 → 따라서 가능한 적은코드로 확실하게 문제를 해결하는 것이 좋음
- 테스트도 결국 “애플리케이션의 정확성을 보장하는 것을 목표” 로 하는 문제해결 코드의 일부로 봐야함 → 테스트 역시 버그에 취약하며 유지보수 해야함
- 커버리지 지표: 테스트 스위트가 소스 코드를 얼마나 실행하는지에 대한 백분율
- 커버리지 지표는 테스트 스위트의 품질을 평가하는데 자주 사용되고 일반적으로 숫자가 높을수록 좋지만, 현실은 그리 간단하지 않음
- 커버리지 지표는 중요한 피드백을 주더라도 테스트 스위트 품질을 효과적으로 측정하는 데 사용될 수 없다
- 즉, 커버리지 지표는 괜찮은 부정 지표이지만, 좋지 않은 긍정 지표이다
- 너무 낮은 커버리지는 “테스트가 충분치 않다” 는 좋은 증거는 맞음
- 하지만, 커버리지가 높다고해서 “양질의 테스트가 많다”는 증거는 될 수 없음
- 가장 많이 사용되는 지표는 코드 커버리지(code coverage) 가 있으며, 테스트 커버리지(test coverage) 로도 불린다
- 코드 커버리지는 코드 수가 적을수록 커버리지가 점점 높아지는데, 이는 테스트 스위트나 기반 코드베이스의 유지보수성이 변경되지 않는다
- ==코드가 적다고 코드가 더 좋아지는게 아니므로
- 다른 커버리지 지표는 분기 커버리지(branch coverage)
- 분기 커버리지는 코드 커버리지의 단점을 극복하는데 도움이 되므로, 더 정확한 결과를 제공한다
- 분기 커버리지 지표는 분기 개수만 다루며, 해당 분기를 구현하는데 얼마나 코드가 필요한지 고려하지 않음
- 같은 if 문을 작성해도 코드커버리지는 if 문 내부의 코드 행 수를, 분기 커버리지는 if 문의 분기 개수(조건 수) 를 판단
- 어떤 커버리지 지표도 의존할 수 없는 이유
- 테스트 대상 시스템의 모든 가능한 결과를 검증한다고 보장할 수 없음
- 외부 라이브러리의 코드 경로를 고려할 수 있는 커버리지는 없음
- 커버리지 지표가 의미가 있으려면 모든 측정 지표(assert문) 을 검증해야 하는데, 모든 대상 코드에 대한 결과를 철저히 검증하는 것이 테스트를 신뢰하거나 좋은 테스트 의 기준이 아님 → 좋은테스트는 “유지보수” 가 생명
- 모든 커버리지 지표가 테스트 대상 시스템이 메서드를 호출할 때 외부 라이브러리가 통과하는 코드 경로를 고려하지 않음
- 커버리지 지표로 테스트가 철저하고 테스트가 충분한지는 전혀 알 수 없음
- 예시) 병원 환자의 체온이 높으면 열이 난다는 것을 의미하며 이는 유용한 관찰(==낮은 커버리지), 그러나 병원이 환자의 적절한 체운을 목표로 해서는 안됨(==잘못된 커버리지 목표)
- 중요) 커버리지 지표는 좋은 부정 지표 이지만, 나쁜 긍정 지표다
- 단, 시스템 핵심부분은 커버리지를 높게 두는 것이 좋다
- 어떻게 테스트 스위트의 품질을 정확하게 측정할까? → 믿을 만한 방법은 스위트 내 각 테스트를 하나씩 따로 평가하는 것
- 중요) 성공적인 테스트 스위트의 특성
- 개발 주기에 통합돼 있다
- 코드베이스에서 가장 중요한 부분만을 대상으로 한다
- 최소한의 유지비로 최대의 가치를 끌어낸다
- 모든 테스트는 개발 주기에 통합돼야 함
- 이상적으로는 코드가 변경될 때마다 아무리 작은 것이라도 실행해야 함
- 테스트가 주는 가치는 테스트 구조 뿐만 아니라 검증하는 코드에도 있음
- 대부분의 애플리케이션에서 가장 중요한 부분은 비즈니스로직(도메인 모델) 이 있는 부분
- 비즈니스 로직 테스트가 시간 투자 대비 최고의 수익을 낼 수 있음
- 그외의 코드들
- 인프라 코드
- 데이터베이스나 서드파티 시스템과 같은 외부 서비스 및 종속성 (e.g. repository, feignclient)
- 모든 것을 하나로 묶는 코드 (e.g. mapper)
- 인프라 코드 같은건 중요 알고리즘이 있을 수 있으므로 이럴땐 테스트를 많이 하는것이 좋다
- 통합 테스트와 같이 일부 테스트는 도메인 모델을 넘어 코드베이스의 중요하지 않은 부분을 포함해 시스템의 전체 동작을 확인할 수 있음
- 가장 기본은 “도메인 모델” 이며, 도메인 모델의 코드베이스도 중요한 부분과 그렇지 않은 부분을 분리해야 함
- 단위 테스트에서 가장 어려운 부분은 최소 유지비로 최대 가치를 달성하는것이며, 이는 이책에서 말하려는 핵심
- 유지비를 상회하는 테스트만 스위트에 유지하는 것이 중요
- 가치 있는 테스트와 가치 없는 테스트 식별하기
- 가치 있는 테스트 작성하기
- 가치 있는 테스트를 작성하려면 코드 설계 기술도 알아야 하며 따라서 책 내용의 상당부는 코드 설계를 설명함
- 단위 테스트와 기반 코드는 서로 얽혀 있으므로 코드베이스에 노력을 많이 기울이지 않으면 가치 있는 테스트를 만들기 힘듦
- 테스트 스위트 내의 모든 테스트를 분석하는 데 사용할 수 있는 기준틀을 설명 (이것이 기초) → ~4장
- 단위 테스트 기술과 실천을 살펴봄 → 4~6장
- 소프트웨어 개발자는 어떤 결정이 내려진 이유를 정확히 설명할 수 없다면 설계 결정에 대해 완전히 인정받지 못한다
- 코드베이스에 변경이 생길때마다 무질서(엔트로피) 도 증가하며 코드는 점점 더 나빠지는 경향이 있고, 테스트는 이러한 경향을 뒤집을 수 있다
- 테스트는 안전망 역할을 하며, 대부분의 회귀에 대한 보험을 제공하는 도구
- 단위 테스트를 작성하는 것이 중요하며, 그중에서도 “좋은 단위테스트” 를 작성하는 것이 중요하다
- 단위 테스트의 궁극적인 목표는 “소프트웨어 프로젝트가 지속적으로 성장하게 하는 것”
- 각각의 테스트는 비용과 편익 요소가 있으므로 모든 테스트를 똑같이 작성 할 필요는 없다
- 테스트 스위트 내에 가치 있는 테스트만 남기고 모두 제거하라
- 애플리케이션과 테스트 코드는 모두 자산이 아니라 부채다
- 단위 테스트를 할 수 없는 코드는 품질이 좋지 못함을 뜻하지만, 단위테스트를 할 수 있다고해서 좋은코드라는 증거는 아니다
- 마찬가지로 커버리지도 낮다는 것은 문제의 징후지만, 높다고해서 테스트 스위트의 품질이 높다는 것은 아니다
- 분기 커버리지로 테스트 스위트의 완전성에 대해 더 나은 인사이트를 얻을 수 있지만, 테스트 스위트가 충분한지는 여전히 알 수 없다
- 특정 커버리지 숫자를 부과하면 동기부여가 잘못된 것 → 커버리지를 위한 테스트를 짤 가능성이 있음
- 시스템의 핵심 부분에 커버리지를 높게 갖는것은 좋지만, 이 높은 수준을 요건으로 삼는것은 좋지 않다
- 성공적인 테스트 스위트의 조건
- 개발주기에 통합돼 있음
- 코드베이스의 핵심 부분을 대상으로 함
- 최소한의 유지비로 최대한의 가치를 끌어냄
- 단위 테스트의 목표를 달성하기 위한 유일한 방법
- 좋은 테스트와 좋지 않은 테스트를 구별하는 방법을 배운다
- 테스트를 리팩터링하여 더 가치있게 만든다
- 단위 테스트에 접근하는 방법은 두가지 견해로 “고전파(classical school)” 와 “런던파(London school)” 로 나뉘었다
- 고전파: 모든 사람이 단위 테스트와 테스트 주도 개발에 원론적으로 접근하는 “고전” 방식
- 런던파: 런던 프로그래밍 커뮤니티에서 시작
- “단위 테스트” 의 정의가 이 둘을 구분짓는 열쇠
- 이 책에선 개인적으로 “고전파” 를 선호
- (중요하지 않은 것들은 제외한) 단위 테스트의 가장 중요한 세가지 속성
- 작은 코드 조각(단위라고도 함)을 검증
- 빠르게 수행
- 격리된 방식으로 처리하는 자동화된 테스트
- “빠른 단위 테스트” 란 매우 주관적인 척도이므로, 실행 시간이 충분하다면 테스트가 충분히 빠르다는 뜻
- 격리 문제는 단위 테스트의 고전파와 런던파를 구분할 수 있게 해주는 근원적 차이
- 고전파 vs 런던파
- 고전적 접근법은 Detroit(디트로이트) 라고도 하며, 단위 테스트에 대한 고전주의적 접근법임, 켄트백의 “테스트주도 개발” 책을 추천
- 런던 스타일은 mockist(목 추종자) 로 불린다
- 런던파에서는 테스트 대상 시스템을 협력자(collaborator) 에게서 격리하는 것을 “격리된 방식” 이라고 말한다
- 즉, 하나의 클래스가 다른 클래스에 의존하면 이 “모든” 의존성을 테스트 대역(test double) 로 대체해야 대상 클래스에만 집중할 수 있다고 함
- 이 방법의 이점들
- 테스트가 실패하면 코드베이스의 어느 부분이 고장 났는지 확실히 알 수 있음 → 다른 의존성을 모두 대체 했기 때문에 테스트가 실패하면 테스트 대상이 문제인 것
- 객체 그래프(object graph) 를 분할할 수 있음 → 의존성을 가진 코드베이스를 테스트하는 것은 어렵지만, 대역을 사용하면 추가 의존성을 주입하지 않고도 테스트 할 수 있음 (단위 테스트 준비를 크게 줄일 수 있음)
- 프로젝트 전반적으로 한 번에 한 클래스만 테스트를 할 수 있음 (테스트 스위트 구조를 간단히 할 수 있음) → 각각의 클래스는 각각의 테스트 단위로 테스트 하기 때문
- 런던 스타일은 테스트 대역(목) 으로 테스트 대상 코드 조각을 분리해서 격리요구 사항을 맞춤
- 단위 테스트에서 “작은 코드 조각을 검증” 한다는 뜻에도 다양한 해석이 가능하지만, 일반적으로 한 번에 한 클래스로 테스트하는 지침을 따르려고 노력해야 함
- 단위테스트는 서로 격리해서 실행해야 테스트를 어떤 순서든 가장 적합한 방식으로 실행할 수 있으며 서로의 결과에 영향을 미치지 않는다
- 각각의 테스트를 격리하는 것은 여러 클래스가 모두 메모리에 상주하고 공유 상태에 도달하지 않는 한, 여러 클래스를 한 번에 테스트 해도 괜찮다는 뜻
- 고전파에선 테스트 간에 공유 상태를 일으키는 의존성에 대해서만 테스트 대역을 사용한다
- 공유 의존성, 비공개 의존성, 프로세스 외부 의존성 + 공유 의존성과 휘발성 의존성
- 공유 의존성(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) 의존성은 각 테스트에서 새 인스턴스를 만들 수 있기만 하면 공유되지않고, 인스턴스는 하나지만 테스트는 이 패턴을 따르지 않으므로 “비공개 의존성” 이다
- 난수 생성기는 휘발성(임시 데이터) 지만, 각 테스트에 별도의 인스턴스를 제공할 수 있으므로 공유 의존성이 아니다
- 공유 의존성은 테스트 대상 클래스 간이 아니라 단위 테스트 간에 공유한다
- 공유 의존성을 대체하는 또 다른 이유는 테스트 실행 속도를 높이는 데 있음
- 데이터베이스나 파일 시스템 등의 공유 의존성에 대한 호출은 비공개 의존성에 대한 호출보다 더 오래 걸림
- 이러한 호출을 포함하는 공유 의존성을 가진 테스트는 단위테스트에서 통합테스트 영역으로 넘어감
- 단위가 반드시 클래스에 국한될 필요는 없음, 공유 의존성이 없는 한 여러 클래스를 묶어서 단위 테스트 할 수도 있음
- 단위테스트를 각각 런던파는 “테스트 대상 시스템에서 협력자를 격리하는것”, 고전파는 “단위 테스트끼리 격리하는 것” 으로 본다
- 정리하면 아래에 대한 의견 차이가 있음
- 격리 요구 사항
- 테스트 대상 코드 조각(단위) 의 구성요소
- 의존성 처리
- 테스트 대역을 어디에서나 흔히 사용할 수 있지만, 런던파는 테스트에서 일부 의존성을 그대로 사용할 수 있도록 함
- 불변 객체(절때 변하지 않는 객체) 는 교체하지 않아도 됨
RemoveInventory(Product.Shampoo, 5)라는 구문에서 ‘5’ 라는 숫자는 변수조차 사용하지 않는다- 이러한 불변 객체를 값 객체(value object) 또는 값(value) 라고 하며, 주요 특징은 각각의 정체성이 없고 내용에 의해서만 식별 된다는 것
- 값 객체는 내용(값)이 동일하다면 무엇을 사용해도 되며, 인스턴스를 서로 바꿔 사용할 수 있다
- 추가적인 궁금증은 https://enterprisecraftsmanship.com/posts/entity-vs-value-object-the-ultimate-list-of-differences/ 를 참조
- (런던파와 고전파가 단위테스트에서 의존성들을 어떻게 생각하는가에 대한 그림)
- 모든 공유 의존성은 변경 가능하지만, 변경 가능한 의존성을 공유하려면 여러 테스트에서 재사용돼야 한다
- 협력자 vs 의존성
- 협력자(collaborator) 는 공유하거나 변경 가능한 의존성
- 데이터베이스는 공유 의존성이므로 “데이터베이스 접근 권한” 을 제공하는 협력자다
- 주입받는 객체 (e.g. Store) 도 시간에 따라 상태가 변할 수 있기 때문에 협력자다
- 숫자 5 같은 불변객체는 의존성이지만 협력자는 아니다 (값 객체다)
- 일반적인 클래스는 “협력자” 와 “값” 이라는 두가지 유형의 의존성으로 동작
- 협력자(collaborator) 는 공유하거나 변경 가능한 의존성
- 중요) 모든 프로세스 외부 의존성이 공유 의존성의 범주에 속하는 것이 아님 → 공유 의존성은 거의 프로세스 외부에 있긴 하지만, 반대로 모든 외부 프로세스가 공유 의존성은 아니라는 뜻
- 테스트가 반환하는 데이터에 영향을 미칠 수 없으면 공유가 아니다
- 대부분의 경우 테스트 속도를 높이려면 테스트 대역으로 교체해야 하지만, 프로세스 외부 의존성이 충분히 빠르고 연결이 안정적이면 테스트에서 그대로 사용해도 괜찮다
- (책에서 달리 명시하지 않는 한, 공유 의존성과 프로세스 외부 의존성이라는 용어는 같다. 왜냐면 실제 프로젝트에서 프로세스 외부가 아닌 공유의존성의 거의 없기 때문)
- 복습하면 고전파와 런던파 간의 주요 차이는 단위 테스트의 정의에서 격리를 어떻게 다루는지에 있다
- (개인적으로 저자는 고전파의 단위테스트를 선호 → 목을 사용하는 테스트는 고전적인 테스트보다 지속 가능한 성장을 달성하는데 더 불안정한 경향이 있기 때문)
- 런던파의 이점
- 입자성(granularity) 이 좋다 → 테스트가 세밀해서(fine-grained) 한번에 한 클래스씩만 확인하기 때문
- 서로 연결된 클래스의 그래프가 커져도 테스트 하기 쉽다 → 의존성이 대역으로 대체되기 때문에
- 테스트가 실패하면 어떤 기능에서 실패한건지 명확히 알 수 있다 → 협력자가 없기때문에 해당 클래스의 기능에서만 오류가 발생함
- 단위 테스트를 런던파는 클래스 단위로 간주한다
- 객체지향 프로그래밍 경력을 가진 개발자들은 보통 클래스를 모든 코드베이스의 기초에 위치한 원자 빌딩 블록(atomic building block) 으로 간주하므로 자연스럽게 클래스를 “테스트에서 검증할 원자단위” 로 취급하게 한다
- 팁) 테스트는 코드의 단위로 검증하면 안되고 문제 영역에 의미가 있는(비즈니스 담당자가 유용하다고 인식할 수 있는) 동작의 단위로 검증해야 한다
- 사실 좋은 코드 입자성을 목표로 하는것은 도움이 되지않고 테스트가 “단일 동작 단위를 검증” 한다면 좋은 테스트다
- 테스트는 프로그래머가 아닌 일반 사람들에게 응집도가 높고 의미가 있는, “해결하는 데 도움이 되는 문제에 대한 이야기”를 들려줘야 한다
- 실제 동작 대신 개별 클래스를 목표로 테스트하면, 테스트가 이상하게 보이기 시작한다
- e.g. 강아지를 부르면 내게 온다 vs 강아지를 부르면 먼저 앞발을 내밀고 뒷발을 움직이고 앞발을 다시 내려놓고 뒷발을 내려놓고 꼬리를 흔들고 …
- 실제 협력자(의존성) 을 대신 목을 사용하면 클래스를 쉽게 테스트 할 수 있다
- 이것을 고전파로 시행하면 대상 시스템 전체 객체 그래프(의존성들) 을 전부 셋팅해야해서 작업량이 많을 수 있다
- (이런것들은 사실이지만) 상호 연결된 클래스의 크고 복잡한 그래프를 작성하는 방법을 찾는것보다 이러한 클래스 그래프들이 만들어지지 않도록 설계하는것이 선행되어야 한다
- 대개 클래스 그래프가 커진 것은 코드 설계 문제의 결과다
- 코드 조각을 단위테스트 하는 능력은 비교적 높은 정확도로 코드 저품질을 예측함
- 목을 사용해서 이러한 복잡한 객체 그래프를 쉽게 테스트 하는것은 문제를 감추기만 할 뿐 근본적인 원인을 해결하지는 못한다
- 런던 스타일 테스트가 있는 시스템에 버그가 생기면, 보통 SUT에 버그가 포함한 테스트만 실패한다
- 고전방식이면 오작동하는 클래스를 참조하는 모든 클래스의 테스트가 실패하게 된다
- 하나의 버그가 전체 시스템에 걸쳐 테스트 실패를 야기하는 것은 원인을 찾기 어렵지만, 마지막으로 수정한 부분이 알아내는것은 크게 어렵지 않으므로 큰 문제가 되지 않는다
- 또한, 하나의 버그가 많은 테스트를 실패로 만들었다면 이는 수정한 코드가 전체 시스템에 의존하는 (큰 가치가 있는) 중요한 코드임을 시사하므로 가치가 있다
- 위에서 설명하지 않은 런던파와 고전파 사이의 남아있는 두가지 차이점
- 테스트 주도 개발(Test-Driven Development; TDD) 를 통한 설계 방식
- 과도한 명세(over-specification) 문제
- 테스트주도개발?
- 테스트 주도 개발은 테스트에 의존해 프로젝트 개발을 추진하는 소프트웨어개발 프로세스
- 테스트 주도개발 프로세스의 세 단계
- 추가해야 할 기능과 어떻게 동작해야 하는지를 나타내는 실패 케이스 작성
- 테스트가 통과할 만큼 충분히 코드를 작성 (이 단계에서 코드가 깨끗하거나 명쾌할 필요는 없음)
- 통과된 테스트의 보호하에서 코드를 안전하고 읽기 쉽게 리팩터링(유지보수 가능한 형태로 수정)
- 런던 스타일은 하향식 TDD (전체 시스템에 대한 기대치를 설정하는 상위 레벨 테스트부터 시작)
- 고전파는 상향식 TDD (도메인 모델을 시작으로 최종 소프트웨어까지 위로) → 의존성이 실제 객체를 다뤄야 하기 때문
- 런던파와 고전파의 중요한 차이점은 과도한 명세(테스트가 SUT 일부 구현 세부 사항에 결합되는 것) 다
- 런던 스타일이 고전 스타일보다 테스트가 구현에 더 자주 결합되는 편 → 의존성의 동작을 mock 해야 하기 때문
- 런던파는 실제 협력자 객체를 사용하는 모든 테스트를 통합 테스트로 간주, 따라서 고전 스타일로 작성된 대부분의 테스트는 런던파 입장에선 통합테스트에 가까움
- (이 책에선 단위 테스트와 통합 테스트의 고전적인 정의를 사용)
- 단위테스트는 다음과 같은 특징을 가짐 (→ 고전파로 번역)
- 작은 코드 조각을 검증 → 단일 동작 단위를 검증
- 빠르게 수행 → 빠르게 수행
- 격리된 방식으로 처리 → 다른 테스트와 별도로 처리
- 통합테스트는 바로 위의 단위 테스트 기준 중 하나를 충족하지 않는 테스트임
- e.g. 공유의존성(데이터베이스 같은) 에 접근하는 테스트는 다른 테스트와 분리해 실행할 수없으므로 통합테스트임
- 프로세스 외부 의존성에 접근하면 테스트가 느려질 수 있음
- 둘 이상의 동작 단위를 검증할 때의 테스트는 통합 테스트임
- 다른 팀이 개발한 모듈이 둘 이상 있을 때 통합 테스트로 어떻게 작동하는지 검증할 수 있음
- 통합 테스트는 시스템 전체를 검증해 소프트웨어 품질을 기여하는 데 중요한 역할을 함
- 통합 테스트는 공유 의존성, 프로세스 외부 의존성뿐 아니라 조직 내 다른 팀이 개발한 코드 등과 통합해 작동하는지도 검증하는 테스트
- 코드가 프로세스 외부 종속성과 함께 어떻게 작동하는지 검증하는 [엔드 투 엔드 테스트] 가 있으며 이는 통합 테스트 종류의 하나다
- 일반적으로 엔드 투 엔드 테스트가 일반적으로 의존성을 더 많이 포함 함
- 일반적으로 통합테스트는 프로세스 외부 의존성을 한두 개만 갖고 작동하지만, 엔드 투 엔드 테스트는 외부 의존성을 전부 혹은 대부분 가지고 작동
- “모든 외부 애플리케이션을 포함해 시스템을 최종 사용자의 관점에서 검증하는 것” 을 엔드 투 엔드 테스트로 본다
- 엔드 투 엔드 테스트는 유지보수 측면에서 가장 비용이 많이 들기 때문에 모든 단위 테스트와 통합 테스트를 통과 한 후 빌드 후반에 실행 하는것이 좋음
- 단위 테스트의 정의
- 단일 동작 단위를 검증
- 빠르게 수행
- 다른 테스트와 별도로 처리
- 격리 문제를 주제로 고전파와 런던파 두 분파로 나뉘며, 이러한 의견 차이는 무엇이 단위를 의미하는지에 대한 관점과 SUT 의 의존성 처리 방식에 영향을 미침
- 런던파는 테스트 대상 단위를 서로 분리하고, 불변 의존성을 제외한 모든 의존성을 테스트 대역으로 대체해야 함
- 고전파는 단위가 아니라 단위 테스트를 서로 분리해야 한다고 주장 (테스트 대상 단위는 코드 가 아니라 동작 단위임 )
- 런던파는 더나은 입자성, 연결된 클래스의 큰 그래프에 대한 테스트 용이성, 실패한 버그를 빠르고 쉽게 찾을 수 있는 장점이 있음
- 다만, 아래 이유들로 인해 런던파의 장점이 희석됨
- 테스트의 단위는 “코드” 가 아니라 “동작” 임
- 코드 조각을 단위 테스트 할 수 없다는 것은 코드 설계에 문제가 있다는 사실을 시사함
- 테스트 실패 후 어디서 어떤 버그가 발생했는지 판단하는것은 크게 어려운 문제가 아님
- 런던파의 가장 큰 문제는 과잉명세(SUT 가 세부 구현에 결합된 테스트) 임
- 통합 테스트는 단위 테스트 기준 3가지 중 하나 이상을 충족하지 못하는 테스트이며, 엔드 투 엔드 테스트도 통합 테스트의 일부임
- 추천 참고 자료
- 고전 스타일을 더 알고싶다면 켄트백의 책 Test-Driven Development: By Example 을 추천
- 런던 스타일을 더 알고싶다면 스티브 프리먼과 냇프라이스의 Growing Object-Oriented Software, Guided by Tests 를 참고
- 의존성 작업을 더 알고싶다면 스티븐 반 듀르센과 마크시먼의 Dependency Injection: Principles, Practices, Patterns 를 추천
- 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 가 아닐때는 역시 준비구절로 하는게 좋다
- 여러개의 준비, 실행, 검증 구절은 테스트가 너무 많은 것을 한번에 검증한다는 의미 → 이러한 테스트는 여러 테스트로 나눠서 해결
- 실행이 하나면 테스트가 단위 테스트 범주에 있게끔 보장하고, 간단하고, 빠르며 이해하기 쉬움
- 통합 테스트에서는 실행 구절을 여러개 두는것도 괜찮을 때가 있음
- 통합테스트는 느릴 수 있는데, 이때 속도를 높이기 위해 여러 개의 통합 테스트를 여러 실행과 검증이 있는 단일 테스트로 묶는것도 방법이 될 수 있음
- 단위 테스트나 충분히 빠른 테스트에선 이러한 테스트 묶기 최적화가 필요하지 않으며, 항상 다단계 단위 테스트를 여러개로 나누는 것이 좋음
- 준비,실행,검증 구절을 여러차례 사용하는것과 비슷하게, if 문을 사용한 테스트가 있는데 이것 또한 안티패턴이다
- 단위든 통합이든 “테스트” 는 “분기가 없는 간단한 일련의 단계” 여야함
- if 문은 테스트가 한번에 너무 많은 것을 검증한다는 표시
- if 문은 테스트를 읽고 이해하는 것을 어렵게하고 추가 유지비만 불어남 → 테스트에 분기가 있음으로 얻는 이점은 없음
- 준비 구절이 가장 큰 경우
- 일반적으로 준비 구절이 세 구절(준비, 실행, 검증) 중에 가장 큼
- 그러나 준비 구절이 지나치게 크면 같은 테스트 클래스 내 비공개 메서드 또는 별도의 팩토리 클래스로 도출하는 것이 좋음
- 준비 구절에서 코드 재사용에 도움이 되는 두가지 패턴으로 “오브젝트 마더(Object Mother) 와 테스트 데이터 빌더(Test Data Builder)” 가 있음
- 실행 구절이 한줄 이상인 경우를 경계하라
- 실행 구절은 보통 한줄
- 실행 구절이 두줄 이상인 경우, SUT의 공개 API 에 문제가 있을 수 있다는 신호이며 두번째 실행로직을 실행하려면 캡슐화가 깨지게 된다
- 실행 구절의 첫번째 메서드 호출과 그 이후의 메서드호출이 있다면, 이후의 메서드 호출을 하지 않을 시 모순이 발생하고 이러한 모순을 불변위반(Invariant violation) 이라 한다
- 잠재적 모순으로 부터 코드를 보호하는 행위를 캡슐화 (encapsulation) 이라 하고, 데이터베이스에 모순이 생기면 큰 문제가 된다 → 해결책은 캡슐화를 항상 지키는 것
- 실행 구절을 한줄로 하는 지침은 비즈니스 로직을 포함하는 대부분 코드에 적용되지만, 유틸리티나 인프라 코드는 덜 적용됨
- 가능한 한 가장 작은 코드를 목표로 하는 전제에 세상에는 “테스트당 하나의 검증을 갖는 지침” 이 있지만 단위 테스트의 단위는 동작의 단위이지 코드의 단위가 아니므로 이는 틀렸다
- 하나의 테스트로 동작의 모든 결과를 평가하는 것이 좋음
- 단, 검증 구절이 너무 커지면 제품 코드에서 추상화가 누락됐다는 증거가 될 수 있으므로, SUT에서 반환된 객체 내에서 모든 속성을 검증하지 말고 클래스 내에 적절한 동등 멤버(equality member) (equals 메서드 같은거) 를 정의하는 것이 좋다
- AAA 패턴 이후에 4번째 구절로 (테스트에 의해 작성된 파일을 지우거나 데이터베이스 연결을 종료하는 등의) 종료 구절을 구분하기도 함
- 하지만 단위테스트는 프로세스 외부에 종속적이지 않으므로 처리해야 할 사이드 이펙트를 남기지 않기때문에, 대부분의 종료 구절이 필요 없다
- 기능 동작은 여러 클래스에 걸쳐있을 만큼 클수도, 단일 메서드로 작을 수도 있지만 그 진입지점은 오직 하나만 존재할 수 있다
- 따라서 SUT의 의존성과 구분하는 것이 중요하고, SUT 가 꽤 많아서 코드에서 대상을 구분히가 힘든 경우, 변수명을
sut로 하는것이 도움이 될 수 있다
- 각각 준비, 실행, 검증 블록들을 빈 줄로 구분하면 대부분의 단위테스트에서 효과적이면서 간결성과 가독성 사이의 균형을 잡을 수 있음
- AAA 패턴을 따르고 준비 및 검증 구절에 빈 줄을 추가하지 않아도 되는 테스트라면 구절 주석들(e.g.
//준비, //검증) 을 제거하라 - 그렇지 않을경우 구절 주석을 유지
- AAA 패턴을 따르고 준비 및 검증 구절에 빈 줄을 추가하지 않아도 되는 테스트라면 구절 주석들(e.g.
- (책에서는 C# 이지만, Java 는 JUnit)
- xUnit 을 선호하는 이유는 대부분 다른 프레임워크보다 깨끗하고 간결하기 때문
- “각 테스트에는 이야기가 있어야 한다” 는 점을 강조하는 어노테이션들을 xUnit 이 보유하고있고, 이는 문제 영역에 대한 개별적이고 원자적(atomic) 인 사실이나 시나리오이며, 테스트가 통과하는것은 이 사실 또는 시나리오가 실제 사실이라는 증거
- 테스트가 제품 코드의 기능을 무조건 나열하는것이 아니라 이런 스토리를 가지도록 사고하는게 좋다 (이 명세는 프로그래머뿐만 아니라 비즈니스 담당자에게도 의미가 있어야 함)
- 테스트 픽스처
- 테스트 픽스처는 테스트 실행 대상 객체다
- 픽스처 객체는 각 테스트 실행 전에 알려진 고전 상태로 유지하기 때문에 동일한 결과를 생성하며, 따라서 이름이 픽스처라고 불린다
- (NUnit 에서
TestFixture테스트가 포함된 클래스를 표시하는 특성으로 불리기도 함)
- 테스트에서 언제 어떻게 코드를 재사용하는지 아는것이 중요하고, 이러한 준비는 별도의 메서드나 클래스로 도출한 후 테스트간에 재사용하는 것이 좋다
- 올바르지 않은 재사용 방법
- 테스트 생성자 (e.g.
SetUp영역)에서 픽스처를 초기화하는 것 - 준비 구절이 동일할 경우
SetUp영역 (JUnit 의@Before같은 동작) 에서 초기화 할 수도 있지만 다음과 같은 단점이 있음- 테스트 간 결합도가 높아짐
- 테스트 가독성이 떨어짐
- 테스트 생성자 (e.g.
setup로직에서의 공통 테스트의 준비 로직을 수정하면 모든 테스트에 영향을 미친다
// 예시
...
@BeforeEach
void setup() {
id = 100L;
}
...- 해당 예시에서
id를100L이 아닌 다른 값으로 수정하면 이유없이 다른 테스트들도 다 실패하게 됨 - 이는 테스트의 중요한 지침인 “테스트를 수정해도 다른 테스트에 영향을 주어서는 안된다” 는 지침을 위반함 → 이 지침을 잘 따르려면 테스트 클래스에 공유 상태를 두지 말아야 함
- 준비 코드를 생성자로 추출할 때의 또다른 단점은 테스트만 보고는 전체 그림을 볼 수 없다. 즉, 테스트 가독성을 떨어뜨리는 것
- 이러한 방법에서 테스트 메서드가 무엇을 하는지 이해하려면 다른 코드도 다 확인해야하고, 독립적인 테스트는 이러한 불확실성을 두지 않는다
- 다른 방법은 테스트 클래스에 비공개 팩토리 메서드(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 {
...
}- 올바른 테스트 명칭은 테스트가 검증하는 내용과 기본 시스템의 동작을 이해하는 데 도움이 되므로 표현력 있는 이름을 붙이는 것이 중요
- 널리 알려진 명명법 중
[테스트 대상 메서드]_[시나리오]_[예상 결과]형식으로 구현 세부사항에 집중하게끔 부추기는 명명법은 사실 도움이 되지 않는다 - 간단하고 쉬운 영어 구문이 훨씬 더 효과적이며 엄격한 명명 구조에 얽매이지 않고 표현력이 뛰어남
- 결국 단위 테스트는 도메인 전문가가 아니라 프로그래머를 위해 프로그래머가 작성한다
public void Sum_of_two_numbers()같이 쉬운 영어로 작성한 이름이 읽기에 훨씬 간결하고, 테스트 대상 동작에 대한 현실적인 설명이다
- 표현력 있고 읽기 쉬운 테스트 이름을 위한 지침
- 엄격한 명명 정책을 따르지 않으며, 표현의 자유를 허용하자
- 문제 도메인에 익숙한 비개발자들에게 시나리오를 설명하는 것처럼 테스트 이름을 짓자
- 단어를 밑줄(underscore)
_로 구분하면 긴 이름에서 가독성을 향상시킬 수 있다
- 클래스 명을 지정할 땐
XXXNameTests패턴을 사용 하지만 테스트가 해당 클래스만 검증하는 것으로 제한하는 것이 아닌, 단위 테스트에서 단위는 동작의 단위이고 클래스 단위가 아님을 명심할 것 - 팁)
XXXNameTest패턴의 테스트 클래스는 동작 단위로 검증할 수 있는 진입점 또는 API 로 여기자
- 테스트명 내 테스트 대상 메서드
- 테스트 이름에 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()- 이게 적당히 잘 지은 메서드명
- 동작이 충분히 복잡하면 이를 설명하는 데 테스트 수가 급격히 증가할 수 있으며 관리가 어려워질 수 있으나, 다행히 대부분의 단위 테스트 프레임워크는 매개변수화된 테스트(parameterized test) 를 사용해 유사한 테스트를 묶을 수 있음
- 매개변수화된 테스트를 사용하면 테스트 코드의 양을 크게 줄일 수 있지만 테스트 메서드가 나타내는 사실을 바로 파악하기 어려워지고, 이는 매개변수가 많을수록 더 어려워짐
- 긍정적인 테스트케이스는 고유한 테스트로 도출하고 가장 중요한 부분을 잘 설명하는 이름을 쓰면 좀 나아짐
- 테스트 코드의 양과 그 코드의 가독성은 서로 상충됨
- 그리고 동작이 너무 복잡하면 매개변수화된 테스트를 조금도 사용하지 말라
- xUnit 에는 시용자가 정의 데이터를 생성하는 데 사용할 수 있는 기능이 있고, 이 기능을 사용하면 컴파일러의 제한을 극복하고 모든 유형의 매개변수를 사용 가능
- 검증문 라이브러리를 사용하면 테스트 가독성을 더욱 높일 수 있음 (
Assert.Equal()대신result.Should().Be()처럼 쓸 수 있음) - 팁) 객체지향프로그래밍(Object-Oriented Programming; OOP) 패러다임은 이야기처럼 읽을 수 있는 방식으로 코드를 구성 할 수 있기에 가독성 이점을 잘 얻어갈 수 있음
- 유일한 단점은 프로젝트에 라이브러리를 추가적으로 사용해야 한다는 것
- 모든 단위 테스트는 AAA 패턴(준비, 실행, 검증) 을 따라야 한다
- 테스트 내 준비나 실행 또는 검증 구절이 여러개 있으면 테스트가 여러 동작 단위를 한번에 검증한다는뜻이며, 이가 단위 테스트라면 각 동작에 하나씩 여러개의 테스트로 쪼개야 한다
- 실행 구절이 한 줄 이상이면 SUT의 API에 문제가 있다는 증거다
- 클라이언트가 항상 실행구절 여러개를 같이 수행해야하고, 이는 잠재적 모순으로 이어질 수 있는데 이것을 “불변 위반”이라고 한다
- 불변 위반으로부터 코드를 보호하는 것을 캡슐화 라고 함
- SUT의 이름을
sut로 지정하여 SUT를 테스트에서 쉽게 구별하도록 하자 - 테스트 픽스처 초기화 코드는 생성자에 두지말고 팩토리 메서드를 도입해서 재사용 하자
- 이러한 재사용은 테스트 간 결합도를 상당히 낮게 유지하고 가독성을 향상시켜줌
- 엄격한 테스트 명명 정책을 지양 하자
- 매개변수화된 테스트로 유사한 테스트에 필요한 코드의 양을 줄일 수 있고, 단점은 테스트 이름을 더 포괄적으로 만들수록 테스트 이름을 읽기 어렵게 되는 것
- 검증문 라이브러리를 사용하면 검증문에서 단어 순서를 재구성해 테스트 가독성을 더 향상시킬 수 있다
2부 개발자에게 도움이 되는 테스트 만들기
- (복습) 좋은 단위 테스트 스위트의 특성
- 개발 주기에 통합되어있음. (실제로 사용하는 테스트만 가치가 있음)
- 코드베이스의 가장 중요한 부분만을 대상으로 함
- 최소 유지비로 최대한의 가치 창출
- 좋은 단위 테스트의 네가지 특성 = 회귀방지, 리팩터링내성, 빠른 피드백, 유지보수성
- 회귀방지 == 소프트웨어 버그
- 코드를 수정한 후 기능이 의도한대로 동작하지 않는 경우를 말함
- 코드는 자산보다 책임에 가까움 → 코드베이스가 커질수록 잠재적인 버그에 더 많이 노출
- 회귀 방지 지표에 대한 테스트 점수 평가 고려사항
- 테스트 중에 실행되는 코드의 양
- 코드 복잡도
- 코드의 도메인 유의성
- 일반적으로, 실행되는 코드가 많을수록 테스트에서 회귀 발생 가능성이 커진다
- 복잡한 비즈니스 로직을 나타내는 코드가 보일러플레이트 코드보다 중요함 → 비즈니스 로직 코드는 버그 발생시 서비스에 큰 타격을 입기 때문
- 우리가 작성하지 않은 코드 (라이브러리, 프레임워크, 외부시스템 등) 도 우리가 작성한 코드 만큼이나 소프트웨어 작동에 영향을 미치므로 중요하다
- 팁) 회귀 방지 지표를 극대화하려면 테스트가 가능한 많은 코드를 실행하는것을 목표로 해야한다
- 리팩터링 내성: 테스트를 “실패”로 바꾸지 않고 기본 애플리케이션 코드를 리팩터링 할 수 있는지에 대한 척도
- 거짓 양성: 실제로 기능이 의도한 대로 작동하지만, 테스트는 실패로 뜨는 경우
- 리팩터링 내성 지표에 대한 테스트 점수 평가 고려사항
- 테스트에서 얼마나 “거짓 양성” 이 발생하는지 살펴봄 (적을수록 좋다)
- 단위테스트 목표는 프로젝트 성장을 지속 가능하게 하는 것
- 테스트가 지속 가능한 성장을 이뤄내는 원리: 회귀 없이 주기적으로 리팩터링하고 새로운기능을 추가할 수 있는 것
- 기능이 고장났을 때 테스트가 조기 경고를 제공하므로
- 코드의 변경이 (테스트로 보호로 인해) 회귀로 이어지지 않을 것이란 확신을 줌
- 거짓양성의 단점
- 테스트가 타당한 이유 없이 실패하면, 코드 문제에 대응하는 능력과 의지가 희석됨
- 더이상 테스트를 신뢰할 수 없게됨
- 테스트와 테스트 대상 시스템(SUT)의 구현 세부 사항이 많이 결합할수록 허위경보가 많이 발생
- 거짓 양성 발생 가능성을 줄이는법 == 구현 세부사항에서 테스트를 분리하는 것
- 테스트를 통해 SUT 가 제공하는 최종결과를 검증하는지 확인할 것
- 테스트는 최종 사용자 관점에서 SUT를 검증하고 사용자에게 의미있는 결과만 확인
- 최종 결과가 바뀌지 않더라도 테스트가 실패하는 경우가 있음 → 테스트가 SUT가 생성한 결과가 아니라 SUT의 구현 세부사항과 결합했기 때문
- 리팩터링 과정은 애플리케이션이 식별할 수 있는 동작에 영향을 주지 않으면서 구현을 변경하는 것
- SUT 구현 세부사항과 결합된 테스트는 리팩터링 내성이 없다 → 이러한 테스트는 리팩터링에 대한 능력과 의지를 방해
- 테스트를 깨지지 않게 하고 리팩터링 내성을 높이는 방법은 SUT의 구현 세부사항과 테스트 간의 결합도를 낮추는 것 뿐
- 즉, 코드의 내부 작업과 테스트 사이를 가능한 멀리 떨어뜨리고 최종 결과를 목표로 하는 것
- SUT 에서 호출하는 메서드의 매개변수가 변경되어 컴파일 오류가 발생하는 알림도 거짓 양성으로 간주한다
- 좋은 단위 테스트의 처음 두 요소(리팩터링 내성과, 회귀방지) 는 본질적으로 정반대 관점에서도 테스트 스위트 정확도에 기여한다
- 프로젝트가 시작된 직후에는 회귀방지가 중요하지만, 리팩터링 내성은 바로 필요하지 않다
- 오류 유형
- 참 음성: 테스트가 통과하고 기본 기능이 의도한 대로 잘 작동하는 상황
- 참 양성: 기능이 고장 나서 테스트가 실패
- 거짓 음성: 기능이 고장났지만 테스트에서 오류가 발생하지 않음 → 회귀방지가 도움이 됨
- 거짓 양성: 기능은 올바르지만 테스트가 여전히 실패로 표시되는 상황 → 리팩터링 내성이 도움이 됨
- 회귀 방지와 리팩터링 내성은 테스트 스위트 정확도를 극대화하는 것을 목표로 한다
- 테스트가 버그 있음을 얼마나 잘 나타내는가(거짓 음성 제외)
- 테스트가 버그 없음을 얼마나 잘 나타내는가(거짓 양성 제외)
- 테스트 정확도를 향상시키는 방법은 두가지 (둘다 매우 중요함)
- 분자(신호) 를 증가시키는 것 → 버그 더 잘 찾아줌
- 분모(소음) 을 줄이는 것 → 엉뚱한 알람을 줄여줌
- 단기적으로는 거짓 양성도 거짓 음성만큼 나쁘지 않다. 그러나 프로젝트가 성장함에 따라 거짓 양성은 테스트 스위트에 점점더 큰 영향을 미침
- 시간이 흐를수록 코드베이스는 더 나빠짐 + 점점 복잡해지고 체계적이지 않게 된다 → 이러한 경향을 줄이려면 주기적으로 리팩터링 해야 함
- 리팩터링이 점점 더 필요해짐에 따라 테스트에서 리팩터링 내성도 점점 더 중요해짐
- 대부분 좋은 단위테스트의 첫번째 특성인 “회귀방지” 에만 중점을 두는 경향이 있다
- 그러나 회귀방지는 “프로젝트의 성장을 유지하는데 도움이 되고, 가치가 있으며, 매우 정확한 테스트 스위트를 구축”하기엔 모자르다
- 중대형 프로젝트에서 작업하면 “거짓 음성” 과 “거짓 양성” 에 대해 똑같이 주의를 기울여야 한다
- 좋은 단위테스트의 (회귀방지, 리팩터링내성 을 제외한) 남은 두가지 특성
- 빠른 피드백
- 유지보수성
- 빠른 피드백은 단위테스트의 필수속성이며, 테스트 속도가 빠를수록 테스트 스위트에서 더 많은 테스트를 자주 수행할 수 있다
- 유지보수성 지표는 다음 두가지 요소로 유지비를 평가한다
- 테스트가 얼마나 이해하기 어려운가
- 테스트는 코드 라인이 적을수록 더 읽기 쉽다
- 테스트 코드의 품질은 제품 코드만큼 중요하다
- 테스트를 작성할 때 절차를 생략하지 말라
- 테스트 코드를 일급 시민으로 취급하라
- 테스트가 얼마나 실행하기 어려운가
- 테스트가 프로세스 외부 종속성으로 작동하면, 의존성을 상시 운영하는데 시간을 들여야 한다
- 테스트가 얼마나 이해하기 어려운가
- 좋은 단위테스트의 4대 특성(회귀방지, 리팩터링내성, 빠른피드백, 유지보수성)을 곱하면 테스트의 가치가 결정된다 (즉, 하나라도 0이면 가치가 0)
- 테스트 코드를 포함한 모든 코드는 책임이다
- 최소 필수값에 대해 상당히 높은 임계치를 설정하고 이 임계치를 충족하는 테스트만 테스트 스위트에 남겨라
- 소수의 매우 가치있는 테스트가 다수의 평범한 테스트보다 프로젝트가 계속 성장하는데 훨씬 더 효과적이다
- 이상적인 테스트는 네 가지 특성 모두에서 최대점수(1점) 을 받는 테스트지만, 안타깝게도 그런 이상적인 테스트를 만들기는 불가능하다
- 왜냐하면, 처음 3가지 특성(회귀방지, 리팩터링내성, 빠른피드백)은 서로 상호 배타적이기 때문 (==뭐가 늘어나면 반대로 뭐가 줄어듦)
- 이 3가지 특성 중 하나를 희생해야 나머지 둘을 최대로 할 수 있다
- 네가지 테스트 특성 중 하나라도 0점을 받는 테스트는 가치가 없다. 따라서, 특성 중 어느 것도 크게 줄지 않는 방식으로 최대한 크게 해야 한다
- 아래는 두 특성을 최대로 하는 것을 목표로 하기위해 나머지 한가지 특성을 최대로 희생(0점)해 가치가 0에 가까워진 극단적인 테스트이다
- 엔드 투 엔드 테스트는 최종 사용자의 관섬에서 시스템을 살펴보며, 일반적으로 UI, 데이터베이스, 외부 애플리케이션을 포함한 모든 구성요소를 거치게 된다
- 특징
- 회귀방지를 훌륭하게 해낸다 → 테스트 하는 코드의 양이 많으므로 대부분의 오류를 발견
- 리팩터링 내성이 훌륭함 → 실제 사용자 관점에서 기능이 어떻게 동작하는지 검증하므로 구현과 결합되어있지 않음
- 속도가 느림 → 많은 양의 코드를 검증하고 실제로 데이터베이스나 외부 애플리케이션과 연동되어있어서 실행 속도가 매우 느림
- 우수한 리팩터링 내성과 빠른 속도를 보장하지만, 그만큼 문제를 잘 잡아내지 못하므로 회귀방지가 떨어짐
- 실행이 빠르고 회귀를 잡을 가능성이 높지만 거짓양성이 많은 테스트(리팩터링 내성이 안좋은) 를 작성하기 매우쉽고, 이를 “깨지기 쉬운 테스트”라 한다
- 깨지기 쉬운 테스트는 무엇보다 “어떻게”에 중점을 두고 있기 때문에 더이상의 리팩터링을 막고 SUT 구현 세부사항에 스며듦 → “어떻게” 아 아닌 “무엇을” 테스트하는지가 중요
- 좋은 단위테스트의 세가지 특성(리팩터링 내성, 회귀방지, 빠른피드백) 은 상호 배타적이다
- 안타깝게도 세가지 모두를 만족시키는 이상적인 테스트를 만들기는 불가능하다
- 마지막 특성인 “유지보수성” 은 엔드투엔드 테스트만 조금 더 높고(외부의존성 설정을 해야하기때문에), 나머지 테스트들은 비슷비슷하다
- (이런 결론을 보고있으면 셋을 정당히 포기하고 맞추는것을 생각하겠지만) “리팩터링내성”은 적당히로 포기할 수 없다
- 리팩터링 내성은 “적당히” 가 아니라 보통 0/1. 즉, 아예 없거나 있거나 둘중하나의 선택이기때문
- 따라서 리팩터링 내성을 최대로 챙기고, 회귀방지(오류를 더 잘 찾을것인가), 빠른피드백(더 빠르게 실행할것인가) 에서 고민해야 한다
- 팁) 테스트 스위트를 단단하게 만들려면, 테스트가 불안한 것 (거짓양성) 을 제거하는것이 최우선 과제
- CAP 정리
- 좋은 단위테스트의 처음 세가지 특성간의 상충관계는 CAP(CAP theorem) 과 유사
- Consistency(일관성): 모든 읽기가 가장 최근의 쓰기 또는 오류를 수신하는것
- Availability(가용성): 모든 요청이 응답을 수신하는 것
- Partition tolerance(분할 내성): 네트워크 분할에도 시스템이 계속 동작하는 것
- CAP 도 마찬가지로 이 셋중에 두가지를 선택하는 절충안을 선택한다
- 마찬가지로 대규모 분산시스템의 “분할내성”은 타협할 수 없다
- 테스트 피라미드는 테스트 스위트에서 테스트 유형간의 비율을 일컫는 개념
- 테스트 피라미드는 종종 세가지유형 (엔드투엔드테스트, 통합테스트, 단위테스트) 로 표현
- 각 층의 넓이는 테스트가 얼마나 많은가(==테스트의수) 즉, 해당 테스트가 얼마나 보편적인지 나타낸다
- 층의 높이는 테스트가 얼마나 최종사용자의 동작을 얼마나 유사하게 흉내내는지 나타낸다
- 피라미드 내 유형에따라 빠른피드백vs회귀방지 사이에서 선택한다
3부 통합 테스트
4부 단위 테스트 안티 패턴
- 관련 추천 도서
- 안티 패턴(anti-pattern): 처음에는 괜찮은 것 같지만 미래에 문제를 야기하는 패턴
- 테스트 스위트(Test suite)
- 테스트 대상 시스템(System Under Test; SUT)
- 테스트 대상 메서드(Method Under Test; MUT): 테스트에서 호출한 SUT의 메서드
- MUT 는 흔히 SUT 와 동의어로 사용하지만, 일반적으로 MUT 는 메서드를, SUT 는 클래스 전체를 가리킨다















