Search

결론 및 총평

작성자
진행일
2023/01/24

더 큰 그림

Chap 1. 단위 테스트의 목표

단위 테스트의 목표는 더 나은 설계가 아닌, 지속 가능한 성장을 가능하게 하는 것이다. 테스트 코드를 작성함으로써 새로운 기능 도입 또는 리팩터링 후 기존 기능이 잘 작동하는지 확인하는데 도움이 된다.
초기 비용이 들겠지만, 장기적으로 보았을 때 그 비용을 메꿀만큼의 장점이 있다. 코드 베이스를 지속적으로 검증하는 테스트 없이는 소프트웨어 개발이 쉽게 확장되지 않는다.
높은 유지 보수 비용으로 인해 가치가 0에 가깝거나 0보다 작은 테스트를 만들기 쉬운데, 우리는 지속 가능한 프로젝트 성장을 위해 고품질 테스트에만 집중해야 한다.
코드 또는 분기 커버러지 지표로 테스트 스위트 품질을 결정하는 데 확실한 보증을 할 수 없다.
테스트 대상 시스템의 모든 가능한 결과를 검증한다고 보증 X ⇒ 커버러지 지표가 의미 있으려면, 모든 측정 지표를 검증해야 함
외부 라이브러리의 코드 경로를 고려할 수 있는 커버러지 지표 X ⇒ 외부 라이브러리의 코드 경로를 고려해야 한다는 것이 아닌, 해당 지표로는 단위 테스트가 좋은지 나쁜지 판단할 수 없음
성공적인 테스트 스위트는 다음과 같은 특성을 지니고 있음
개발 주기에 통합되어 있다.
코드 베이스에서 가장 중요한 부분만을 대상으로 한다.
최소한의 유지비로 최대의 가치를 끌어낸다.
단위 테스트의 목표를 달성하기 위한 유일한 방법은 다음과 같다.
좋은 테스트와 좋지 않은 테스트를 구별하는 방법을 배운다.
테스트를 리팩터링해서 더 가치 있게 만든다.

Chap 2. 단위 테스트란 무엇인가

단위 테스트란 단일 동작 단위를 검증하고, 빠르게 수행하고, 다른 테스트와 별도로 처리하는 것을 뜻한다.
격리 문제를 중심으로 고전파(디트로이트)와 런던파(목 추종자)로 두 개의 단위 테스트 분파로 나뉘었다. 이러한 의견 차이는 무엇이 단위를 의미하는지에 대한 관점과 테스트 대상 시스템(SUT)의 의존성 처리 방식에 영향을 미친다.
고전파
고전파는 단위가 아니라 단위 테스트를 서로 분리해야 한다고 한다. 또한 테스트 대상 단위는 코드 단위가 아닌 동작 단위이다. 따라서 공유 의존성만 테스트 대역으로 대체해야 한다. 공유 의존성을 테스트가 서로 실행 흐름에 영향을 미치는 수단으로 제공하는 의존성이다.
런던파
런던파는 테스트 대상 단위를 서로 분리해야 한다고 한다. 테스트 대상 단위는 코드의 단위, 보통 단일 클래스다. 불변 의존성을 제외한 모든 의존성을 테스트 대역으로 대체해야 한다.
런던파는 입자성이 좋고, 서로 연결된 클래스의 그래프가 커져도 테스트 하기가 쉽고, 테스트가 실패하면 어떤 기능이 실패했는지 확실히 알 수 있는 등의 장점을 가지고 있다.
런던파의 장점이 처음에는 매력적으로 다가올 수 있지만, 몇 가지 문제가 있다.
1.
테스트 대상 클래스에 대한 초점이 잘못됐다. 테스트는 코드 단위가 아닌 동작 단위를 검증해야 한다. 또한, 코드 조각을 단위 테스트할 수 없다는 것은 코드 설계에 문제가 있다는 사실을 알려주는 강한 징후다.
2.
테스트 실패 후 어떤 기능에 버그가 있는지 판단하는 것이 도움은 되지만, 종종 버그의 원인을 알고 있기 때문에, 그리 큰 문제는 아니다. 즉, 바로 마지막에 수정한 것이 버그의 원인일 것이다.
3.
과잉 명세, 즉 SUT 세부 구현에 결합되는 테스트 문제가 발생한다.
통합 테스트는 단위 테스트 기준 중 하나 이상을 충족하지 못하는 테스트이다. 앤드 투 엔드 테스트는 통합 테스트의 일부이다. 최종 사용자의 관점에서 시스템을 검증한다.
엔드 투 엔드 테스트는 애플리케이션과 함께 작동하는 프로세스 외부 의존성의 전부 또는 대부분에 직접 접근한다.

Chap 3. 단위 테스트 구조

모든 단위 테스트는 AAA 패턴(준비, 실행, 검증)을 따라야 한다. 테스트 내 준비나 실행 또는 검증 구절이 여러개 있으면, 테스트가 여러 동작 단위를 한 번에 검증한다는 표시이다. 이 테스트가 단위 테스트라면 각 동작에 하나씩 여러 개의 테스트로 나눠야 한다.
여러 개의 준비(Arrange), 실행(Act), 검증(Assert) 구절이 있다는 것은 테스트가 너무 많은 것을 한번에 검증한다는 뜻이다. 이러한 테스트는 여러 테스트로 나눠 작업하는 것이 좋다.
여러 개의 동작 단위를 검증하는 테스트 구조는 피하는 것이 좋다. 실행이 하나만 있다면 단위 테스트 범주에 있게끔 보장하고, 간단하고, 빠르며, 이해하기 쉽다. 즉, 각 동작을 고유의 테스트로 도출하자.
테스트 픽스처 초기화 코드는 생성자에 두지 말고 팩토리 메서드를 도입해서 재사용하자. 이러한 재사용은 테스트 간 결합도를 상당히 낮게 유지하고 가독성을 향상시킨다.
엄격한 테스트 명명 정책을 시행하지 말자. 문제 도메인에 익숙한 비개발자들에게 시나리오를 설명하는 것처럼 각 테스트의 이름을 지정하자. 테스트 이름에서 밑줄 표시로 단어를 구분하고, 테스트 대상 메서드 이름을 넣지 말자.
매개변수화된 테스트로 유사한 테스트에 필요한 코드의 양을 줄일 수 있다. 단점은 테스트 이름을 더 포괄적으로 만들수록 테스트 이름을 읽기 어렵게 하는 것이다.
검증문 라이브러리를 사용하면, 쉬운 영어처럼 읽도록 검증문에서 단어 순서를 재구성하여 테스트 가독성을 더욱 향상시킬 수 있다.

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

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

좋은 단위 테스트에는 자동화된 테스트를 분석하는 데 사용할 수 있는 네 가지 기본 특성이 있다.
회귀 방지
회귀 방지는 테스트가 얼마나 버그(회귀)의 존재를 잘 나타내는지에 대한 척도이다. 테스트가 코드를 더 많이 실행할수록 테스트에서 버그가 드러날 확률이 더 높아진다.
리팩터링 내성
리팩터링 내성은 테스트가 거짓 양성을 내지 않고 애플리케이션 코드 리팩터링을 유지할 수 있는 정도를 의미한다.
거짓 양성은 허위 경보다. 즉, 테스트가 실패했다고 나타내지만 그 기능은 의도한대로 작동한다. 거짓 양성은 테스트 스위트에 치명적인 영향을 줄 수 있다. 또한, 거짓 양성은 테스트 대상 시스템의 내부 구현 세부 사항과 테스트 간 강결합의 결과이다. 결합도를 낮추려면 SUT가 수행한 단계가 아니라 SUT가 만든 최종 결과를 검증해야 한다.
회귀 방지와 리팩터링 내성은 테스트 정확도에 기여한다. 테스트는 가능한 한(리팩터링 내성 영역) 적은 소음(거짓 양성)으로 강한 신호(버그를 찾을 수 있음. 회귀 방지 영역)를 발생시키기 때문에 정확하다.
리팩터링 내성은 타협할 수 없다. 테스트에 이 속성이 있는지 여부는 대부분 이진 선택, 즉 리팩터링 내성을 갖고 있거나 갖고 있지 않거나 둘 중 하나이기 때문이다. 특성 간의 절충은 회귀 방지와 빠른 피드백 사이의 선택으로 귀결된다.
빠른 피드백
빠른 피드백은 테스트가 얼마나 빨리 실행되는지에 대한 척도이다.
유지 보수성
테스트 이해 난이도. 테스트가 작을 수록 읽기 쉽다.
테스트 실행 난이도. 테스트에 관련된 프로세스 외부 의존성은 적을수록 쉽게 운영할 수 있다.
테스트 피라미드는 단위 테스트, 통합 테스트, 엔드 투 엔드 테스트의 일정한 비율을 일컫는다.
피라미드에서는 테스트 유형마다 빠른 피드백과 회귀 방지 사이에서 다른 선택을 한다. 엔드 투 엔드 테스트는 회귀 방지를 선호하는 데 반해, 단위 테스트는 빠른 피드백을 선호한다.
테스트를 작성할 때는 블랙박스 테스트 방법을 사용하고, 테스트를 분석할 때는 화이트 박스 방법을 사용하자.
블랙박스 테스트: 시스템의 내부 구조를 몰라도 시스템의 기능을 검사할 수 있는 방법. 애플리케이션이 어떻게 해야 하는지가 아니라 무엇을 해야 하는지를 중심으로 구축된다.
화이트박스 테스트: 정반대이다. 애플리케이션 내부 작업을 검증하는 방식이며, 테스트는 요구 사항이나 명세가 아닌 소스 코드에서 파생된다.

Chap 5. 목과 테스트 취약성

테스트 대역에는 더미, 스텁, 스파이, 목, 페이크 등의 다섯 가지 변형이 있는데, 이는 다시 목과 스텁 두 가지 유형으로 분류할 수 있다. 스파이는 기능적으로 목과 같고, 더미와 페이크는 스텁과 같은 역할을 한다.
목은 외부로 나가는 상호작용을 모방하고 검사(SUT에서 의존성로서의 호출로, 해당 의존성의 상태를 변경)하는 데 도움이 된다.
스텁은 내부로 들어오는 상호 작용을 모방(SUT가 해당 의존성을 호출해 입력 데이터를 가져옴)하는 데 도움이 된다.
목 라이브러리의 클래스는 실제 목을 만드는 데 도움이 되지만, 그 자체로는 목이 아니다. 도구로서의 목을 사용하여 목과 스텁, 두 가지 유형의 테스트 대역을 생성할 수 있기 때문에 도구로서의 목과 테스트 대역으로서의 목을 혼동하지 않는 것이 중요하다.
스텁과의 상호 작용을 검증하면 취약한 테스트로 이어진다. 이러한 상호 작용은 최종 결과와 일치하지 않는다. 이는 결과로 가는 중간 단계로, 구현 세부 사항에 해당한다.
CQS(명령 조회 분리) 원칙에 따르면, 모든 메서드가 명령 또는 조회 중 하나여야 하지만 둘 다는 안된다. 명령을 대체하는 테스트 대역은 목이다. 조회를 대체하는 테스트 대역은 스텁이다.
모든 프로덕션 코드는 공개 API인지 비공개 API인지식별할 수 있는 동작인지 구현 세부 사항인지라는 두 가지 차원으로 분류할 수 있다. 코드의 공개성은 private, public, internal 키워드 등 접근 제한자에 의해 제어된다. 다음 요구사항을 하나라도 충족하면 식별할 수 있는 동작이다.
클라이언트가 목표를 달성하는 데 도움이 되는 연산을 노출하라. 연산은 계산을 수행하거나 부작용을 초래하거나 또는 둘 다 하는 메서드다.
클라이언트가 목표를 달성하는 데 도움이 되는 상태를 노출하라. 상태는 시스템의 현재 상태다.
잘 설계된 코드는 식별할 수 있는 동작이 공개 API와 일치하고, 구현 세부 사항이 비공개 API 뒤에 숨겨져 있는 코드이다. 공개 API가 식별할 수 있는 동작 이상으로 커지면 코드는 구현 세부 사항을 유출한다.
캡슐화는 코드를 불변성 위반으로부터 보호하는 행위다. 클라이언트는 구현 세부 사항을 사용해 코드의 불변성을 우회할 수 있기 때문에 구현 세부 사항을 노출하면 캡슐화가 위반되는 경우가 종종 있다.
육각형 아키텍처는 상호 작용하는 애플리케이션의 집합이고 각 애플리케이션은 육각형으로 표시한다. 각 육각형은 도메인과 애플리케이션 서비스라는 두 계층으로 구성된다.
그림 5.8 대표적인 애플리케이션은 도메인 계층과 애플리케이션 계층으로 구성된다.
도메인과 애플리케이션 서비스 계층 간의 영향 분리.
도메인 계층은 비즈니스 로직을 책임져야 하고, 애플리케이션 서비스는 도메인 계층과 외부 애플리케이션 간의 작업을 조정해야 한다.
애플리케이션 서비스 계층에서 도메인 계층으로의 단방향 의존성 흐름.
도메인 계층 내 클래스는 서로에게만 의존해야 하고, 애플리케이션 서비스 계층의 클래스에 의존해서는 안된다.
외부 애플리케이션은 애플리케이션 서비스 계층이 유지하는 공통 인터페이스를 통해 연결된다. 아무도 도메인 계층에 직접 액세스 할 수 없다.
육각형의 각 계층은 식별할 수 있는 동작을 나타내며 각각의 구현 세부 사항이 있다.
애플리케이션에는 시스템 내부 통신과 시스템 간 통신이라는 두 가지 통신 유형이 있다.
시스템 내부 통신: 애플리케이션 내 클래스 간 통신
시스템 간 통신: 애플리케이션이 외부 애플리케이션과 통신할 때
시스템 내 통신은 구현 세부 사항이다. 애플리케이션을 통해서만 접근할 수 있는 외부 시스템을 제외하고 시스템 간 통신은 식별할 수 있는 동작이다. 애플리케이션을 통해서만 접근할 수 있는 외부 시스템과의 상호 작용도 구현 세부 사항인데, 그 결과의 부작용은 외부에서 확인할 수 없기 때문이다.
시스템 내 통신을 검증하고자 목을 사용하면 취약한 테스트로 이어진다. 따라서 시스템 간 통신(애플리케이션 경계를 넘는 통신)과 해당 통신의 부작용이 외부 환경에서 보일 때만 목을 사용하는 것이 타당하다.

Chap 6. 단위 테스트 스타일

출력 기반 테스트
출력 기반 테스트는 SUT에 입력을 주고 출력을 확인하는 테스트 스타일이다. 이 테스트 스타일은 숨은 입출력이 없다고 가정하고, SUT 작업의 결과는 반환하는 값 뿐이다.
상태 기반 테스트
상태 기반 테스트는 작업이 완료된 후의 시스템 상태를 확인한다.
통신 기반 테스트
통신 기반 테스트는 목을 사용해서 테스트 대상 시스템과 협력자 간의 통신을 검증한다.
출력 기반 테스트가 품질이 가장 좋다. 이러한 테스트는 구현 세부 사항에 거의 결합되지 않으므로 리팩터링 내성이 있다. 또한 작고 간결하므로 유지 보수하기 쉽다.
상태 기반 테스트는 안정성을 위해 더 신중해야 한다. 단위 테스트를 하려면 비공개 상태를 노출하지 않도록 해야 한다. 상태 기반 테스트는 출력 기반 테스트보다 크기가 큰 편이므로 유지 보수가 쉽지 않다. 헬퍼 메서드와 값 객체를 사용해 유지 보수성 문제를 완화할 수도 있지만 제거할 순 없다.
통신 기반 테스트는 안정성을 위해 더 신중해야 한다. 애플리케이션 경계를 넘어서 외부 환경에 부작용이 보이는 통신만 확인해야 한다. 통신 기반 테스트의 유지 보수성은 출력 기반 테스트 및 상태 기반 테스트와 비교할 때 좋지 않다. 목은 공간을 많이 차지하는 경향이 있어서 테스트 가독성이 떨어진다.
함수형 프로그래밍은 수학적 함수(순수 함수)를 사용한 프로그래밍이다. 수학적 함수는 호출 횟수에 상관없이 주어진 입력에 대해 동일한 출력을 생성한다. 즉, 숨은 입출력이 없는 함수이다.
부작용 → 메서드 시그니처에 표시되지 않은 출력
예외 → 호출된 예외는 호출 스택의 어느 곳에서도 발생할 수 있으므로, 메서드 시그니처가 전달하지 않는 출력을 추가한다.
내외부 상태에 대한 참조 → 정적 속성을 사용한 값을 가져오는 메서드, DB에서 데이터 질의 등, 모두 메서드 시그니처에 없는 실행에 대한 입력이며 숨어있다.
함수형 프로그래밍의 목표는 비즈니스 로직과 부작용을 분리하는 것이다. 함수형 아키텍처는 부작용을 비즈니스 연산의 가장자리로 밀어내 분리를 이루는 데 도움이 된다. 이 방법으로, 부작용을 다루는 코드를 최소화하면서 순수 함수 방식으로 작성된 코드의 양을 최대화 할 수 있다.
함수형 아키텍처에서 함수형 코어는 수학적 함수를 사용해 구현되며 애플리케이션에서 모든 결정을 내린다. 가변 셸은 함수형 코어에 입력 데이터를 제공하고 DB와 같은 프로세스 외부 의존성에 부작용을 적용해 그 결정을 해석한다.
함수형 아키텍처는 모든 코드를 함수형 코어와 가변 셸이라는 두 가지 범주로 나눈다. 가변 셸은 입력 데이터를 함수형 코어에 공급하고, 코어가 내린 결정을 부작용으로 변환한다.
함수형 아키텍처와 육각형 아키텍처의 차이는 부작용의 처리에 있다. 함수형 아키텍처는 모든 부작용을 도메인 계층 밖으로 밀어낸다. 이와 반대로, 육각형 아키텍처는 도메인 계층에만 한정돼 있는 한, 도메인 계층에 의해 만들어진 부작용도 괜찮다.
함수형 아키텍처와 전통적인 아키텍처 사이의 선택은 성능과 코드 유지 보수성 사이의 절충이며, 함수형 아키텍처는 유지 보수성 향상을 위해 성능을 희생한다.
모든 코드베이스를 함수형 아키텍처로 전환할 수는 없다. 함수형 아키텍처를 전략적으로 사용하자. 시스템의 복잡도와 중요성을 고려하여, 코드베이스가 단순하거나 그렇게 중요하지 않으면 함수형 아키텍처에 필요한 초기 투자는 별 효과가 없다.

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

코드를 리팩터링하지 않고는 테스트 스위트를 크게 개선할 수 없다.
코드 복잡도는 코드에서 의사 결정 지점 수에 따라 명시적으로(코드) 그리고 암시적으로(코드가 사용하는 라이브러리) 정의된다.
도메인 유의성은 프로젝트의 문제 도메인에 대해 코드가 얼마나 중요한지를 보여준다. 복잡한 코드는 종종 도메인 유의성이 높고 그 반대의 경우도 있지만, 모든 경우에 100% 해당하지는 않는다.
복잡한 코드와 도메인 유의성을 갖는 코드는 해당 테스트의 회귀 방지가 뛰어나기 때문에 단위 테스트에서 가장 이롭다.
협력자가 많은 코드를 다루는 단위 테스트는 유지비가 많이 든다. 협력자가 많은 코드는 테스트 비용이 많이 들기 때문에, 협력자의 유형이나 수를 잘 고려해야 한다.
모든 프로덕션 코드는 복잡도 또는 도메인 유의성과 협력자 수에 따라 네 가지 유형의 코드로 분류할 수 있다.
도메인 모델 및 알고리즘은 단위 테스트에 대한 노력 대비 가장 이롭다 → 복잡도 또는 도메인 유의성이 높음. 협력자가 거의 없음
간단한 코드는 테스트할 가치가 전혀 없다 → 복잡도와 도메인 유의성이 낮음. 협력자가 거의 없음.
컨트롤러는 통합 테스트를 통해 간단히 테스트해야 한다 → 복잡도와 도메인 유의성이 낮음. 협력자가 많음.
지나치게 복잡한 코드는 컨트롤러와 복잡한 코드로 분할해야 한다 → 복잡도 또는 도메인 유의성이 높음. 협력자가 많음.
즉, 코드가 중요하거나 복잡할수록 협력자가 적어야 한다.
험블 객체 패턴은 해당 코드에서 비즈니스로 로직을 별도의 클래스로 추출해 복잡한 코드를 테스트할 수 있는데 도움이 된다. 이로써 나머지 코드는 비즈니스 로직을 둘러싼 얇은 험블 래퍼, 즉 컨트롤러가 된다. 이 험블 래퍼가 테스트하기 어려운 의존성과 새로 추출된 컴포넌트를 불이지만 자체적인 로직이 거의 없으므로 테스트할 필요가 없다.
육각형 아키텍처, 함수형 아키텍처는 험블 패턴을 구현하고 있다. 육각형 아키텍처는 비즈니스 로직과 프로세스 외부 의존성과의 통신을 분리하도록 한다. 함수형 아키텍처는 프로세스 외부 의존성뿐만 아니라 모든 협력자와의 통신과 비즈니스 로직을 분리한다.
가치 있는 단위 테스트를 위해서 리팩터링을 할 때 코드의 깊이와 너비의 관점에서 비즈니스 로직과 오케스트레이션 책임을 생각하자. 코드는 깊을 수도 있고(복잡하거나 중요함) 넓을 수도 있지만(협력자가 많음), 둘 다는 아니다. 도메인 유의성이 있으면 전제 조건을 테스트하고, 그 외의 경우에는 테스트하지 않는다.
컨트롤러에서 조건부 로직을 처리하면서 동시에 프로세스 외부 협력자 없이 도메인 계층을 유지 보수하는 것은 까다롭고 절충이 있기 마련이다. 우리는 이 때 세 가지 특성의 균형을 맞추는 것이 중요하다.
도메인 모델 테스트 유의성: 도메인 클래스 내 협력자 수와 유형에 대한 함수
컨트롤러 단순성: 의사 결정(분기) 지점이 있는지에 따라 다름
성능: 프로세스 외부 의존성에 대한 호출 수로 정의
위에서 언급한 방법은 세 가지 특성 중 최대 두 가지 특성만 가질 수 있다.
외부에 대한 모든 읽기와 쓰기를 비즈니스 연산 가장자리로 밀어내기: 컨트롤러를 단순하게 유지하고 도메인 모델 테스트 유의성을 지키지만, 성능이 저하된다.
도메인 모델에 프로세스 외부 의존성을 주입하기: 성능을 유지하고 컨트롤러를 단순하게 하지만, 도메인 모델의 테스트 유의성이 떨어진다.
의사 결정 프로세스 단계를 더 세분화하기: 성능과 도메인 모델 테스트 유의성을 지키지만, 컨트롤러의 단순함을 포기한다.
의사 결정 프로세스 단계를 더 세분화하는 것이 장단점을 고려할 때 가장 효과적인 절충이다. 다음 두 가지 패턴을 사용해 컨트롤러 복잡도 증가를 완화할 수 있다.
CanExecute/Execute 패턴을 사용하여 비즈니스 로직이 도메인 모델에서 컨트롤러로 유출되는 것을 방지
도메인 이벤트는 도메인 모델의 중요한 변경 사항을 추적하고 해당 변경 사항을 프로세스 외부 의존성에 대한 호출로 변환환다. 이 패턴으로 컨트롤러에서 추적에 대한 책임이 없어진다.
외부 시스템에 대한 애플리케이션의 부작용을 추상화하는 것이 7장에서 살펴본 점이다. 비즈니스 연산이 끝날 때까지 이런 부작용을 메모리에 둬서 추상화하고, 프로세스 외부 의존성 없이 단순한 단위 테스트로 테스트할 수 있었다.
추상화 할 것을 테스트하기 보다 추상화를 테스트하는 것이 더 쉽다.

통합 테스트

Chap 8. 통합 테스트를 하는 이유

통합 테스트는 단위 테스트가 아닌 테스트에 해당한다. 통합 테스트는 시스템이 프로세스 외부 의존성과 통합돼 작동하는 방식을 검증한다.
통합 테스트는 컨트롤러를 다루고, 단위 테스트는 알고리즘과 도메인 모델을 다룬다.
통합 테스트는 회귀 방지와 리팩터링 내성이 우수하고, 단위 테스트는 유지 보수성과 피드백 속도가 우수하다.
단위 테스트와 통합 테스트 간 균형을 유지하는 것이 좋다.
통합 테스트가 프로세스 외부 의존성과 직접 작동하면 느려지며, 유지비가 많이 든다.
유지비가 증가하는 이유
프로세스 외부 의존성 운영이 필요함
관련된 협력자가 많아서 테스트가 비대해짐
통합 테스트는 코드를 많이 거치므로 회귀 방지가 단위 테스트보다 우수하다. 또한 프로덕션 코드와의 결합도가 낮아 리팩터링 내성도 우수하다.
그림 8.2 테스트 피라미드는 대부분의 애플리케이션에 가장 적합한 절충안을 나태난다. (2장에서 설명했듯이 엔드 투 엔드 테스트는 통합 테스트의 하위 집합이다.)
단위 테스트와 통합 테스트의 비율은 프로젝트 특성에 따라 다를 수 있지만, 단위 테스트로 가능한 한 많이 비즈니스 시나리오의 예외 상황을 확인하고, 통합 테스트는 주요 흐름(happy path)과 단위 테스트가 다루지 못하는 기타 예외 상황(edge case)을 다룬다.
주요 흐름은 시나리오의 성공적인 실행이다. 예외 상황은 비즈니스 시나리오 수행 중 오류가 발생하는 경우이다.
빠른 실패 원칙은 버그가 빠르게 나타날 수 있도록 하며 통합 테스트에서 할 수 있는 대안이다.
통합 테스트에서 프로세스 외부 의존성과의 상호 작용을 모두 확인하려면 가장 긴 주요 흐름을 선택해라
단위 테스트에 다룰 수 없는 예외 상항이 있듯이 이 부분도 예외가 있다.
통합 테스트는 시스템이 프로세스 외부 의존성과 어떻게 통합되는지를 검증한다.
실제 프로세스 외부 의존성 사용
해당 의존성을 목으로 대체
프로세스 외부 의존성에는 두 가지 유형이 있다.
관리 의존성(전체를 제어할 수 있는 프로세스 외부 의존성)
이러한 의존성은 애플리케이션을 통해서만 접근할 수 있으며, 해당 의존성과의 상호 작용은 외부 환경에서 볼 수 없다(ex: DB).
비관리 의존성(전체를 제어할 수 없는 프로세스 외부 의존성)
해당 의존성과의 상호 작용을 외부에서 볼 수 있다(ex: SMTP 서버, 메시지 버스 등).
관리 의존성과의 통신은 구현 세부 사항이고, 비관리 의존성과의 통신은 식별할 수 있는 동작이다. 즉, 관리 의존성은 실제 인스턴스를 사용하고, 비관리 의존성은 목으로 대체하라.
때로는 관리 의존성과 비관리 의존성 모두의 특성을 나타내는 프로세스 외부 의존성이 있다. 예를 들어, 다른 애플리케이션이 접근할 수 있는 데이터베이스가 있다. 비관리 의존성의 식별 가능한 부분을 비관리 의존성으로 간주하고, 테스트에서 해당 부분을 목으로 대체하라. 나머지 부분을 관리 의존성으로 간주하고, 해당 부분 과의 상호 작용 대신 최종 상태를 검증하자.
그림 8.5 외부 애플리케이션에서 볼 수 있는 데이터베이스 부분을 비관리 의존성으로 처리하라. 통합 테스트에서 목으로 대체한다. 나머지 데이터베이스는 관리 의존성으로 취급하라. 상호 작용이 아닌 최종 상태를 검증하라.
의존성 추상화를 위해 인터페이스를 사용하자. 단위 테스트 영역에서 가장 많이 오해하는 것 중 하나가 인터페이스 사용이다.
단일 구현을 위한 인터페이스는 추상화가 아니며, 해당 인터페이스를 구현하는 구체 클래스보다 결합도가 낮지 않다 ⇒ 진정한 추상화는 발견하는 것이지, 발명하는 것이 아니다.
단일 구현 인터페이스에 대한 향후 구현을 예상하면 YAGNI 원칙을 위배한다.
YAGNI(You Aren’t Gonna Need It) - 현재 필요하지 않은 기능에 시간을 들이지 말라
향후 기능이 어떤지 설명하려고 기능을 개발해서도, 기존 코드를 수정해서도 안된다.
현재 비즈니스 담당자들에게 필요하지 않은 기능에 시간을 보낸다면, 지금 당장 필요한 기능을 제치고 시간을 허비하는 것이다 → 기회 비용
프로젝트 코드가 적을 수록 좋다.
단일 구현 인터페이스를 사용하기에 타당한 이유는 목을 사용하기 위한 것뿐이다. 비관리 의존성에만 사용하고, 관리 의존성은 구체 클래스를 사용하자.
프로세스 내부 의존성에 대해 단일 구현 인터페이스는 좋지 않다. 이러한 인터페이스는 목을 사용해 도메인 클래스 간의 상호 작용을 확인하게 되고, 테스트가 코드의 구현 세부 사항에 결합된다.
통합 테스트를 최대한 활용하는 데 도움이 되는 몇 가지 일반적인 지침이 있다.
도메인 모델 경계 명시하기
도메인 모델을 코드베이스에 명시적이고 잘 알려진 위치에 둬라. 도메인 클래스와 컨트롤러 사이의 경계가 명확하면 단위 테스트와 통합 테스트를 좀 더 쉽게 구분할 수 있다.
애플리케이션 내 계층 줄이기
간접 계층이 너무 많으면 코드를 추론하기가 어려워진다. 간접 계층을 가능한 한 적게 하라. 대부분의 백엔드 시스템은 도메인 모델, 애플리케이션 서비스 계층(컨트롤러), 인프라 계층, 이 세 가지 계층만 있다.
순환 의존성 제거하기
순환 의존성이 있으면 코드를 이해하려고 할 때 알아야 하는 부담이 커진다. 대표적인 예는 콜백이다. 값 객체를 도입해 순환을 없애고, 호출부에 주는 결과를 값 객체로 반환하라.
테스트에 여러 실행 구절이 있는 것은 올바른 상태가 되기 어려운 프로세스 외부 의존성으로 작동하는 경우에만 타당하다. 단위 테스트는 프로세스 외부 의존성으로 수행되지 않기 때문에 여러 가지 실행을 해서는 안된다. 다단계 테스트는 대부분 엔드 투 엔드 테스트 범주에 속한다.
지원 로깅은 지원 부서나 시스템 관리자를 위한 것이며, 애플리케이션의 식별할 수 있는 동작이다. 진단 로깅은 개발자가 애플리케이션 내부에서 진행되는 작업을 이해하는 데 도움을 주며, 구현 세부 사항이다.
지원 로깅은 비즈니스 요구 사항이므로 해당 요구 사항을 코드베이스에 명시적으로 반영하자. 비즈니스에 필요한 모든 지원 로깅이 나열돼 있는 특별한 DomainLogger 클래스를 도입하라.
지원 로깅을 프로세스 외부 의존성으로 작동하는 다른 기능처럼 취급하자. 도메인 이벤트를 사용해 도메인 모델의 변경 사항을 추적해야 한다. 컨트롤러에서 도메인 이벤트를 DomainLogger 호출로 변환하자.
진단 로깅 테스트는 지양하자. 지원 로깅과 달리 도메인 모델에서 직접 진단 로그를 남길 수도 있다. 진단 로깅은 가끔 사용하자. 진단 로깅을 너무 많이 쓰면 코드를 복잡하게 하고 로그의 신호 대비 잡음 비율이 나빠진다. 이상적으로는 진단 로깅을 처리되지 않은 예외에 대해서만 사용해야 한다.
항상 모든 의존성(로거 포함)을 생성자 또는 메서드 인수를 통해 명시적으로 주입하자.

Chap 9. 목 처리에 대한 모범 사례

9장에서는 목에 대해 리팩터링 내성과 회귀 방지를 최대화해서 최대 가치의 통합 테스트를 개발하는데 도움이 되는 지침에 대해서 알아본다.
시스템 끝에서 상호 작용 검증하기
목을 사용할 때는 “시스템 끝에서 비관리 의존성과의 상호 작용을 검증하라”를 항상 유의하자. 컨트롤러와 비관리 의존성 사이의 타입 사슬에는 마지막 고리를 목으로 처리하자. 이로써 회귀 방지(통합 테스트로 검증된 코드가 더 많기 때문)와 리팩터링 내성(코드의 구현 세부 사항에서 목을 분리하기 때문)이 향상될 수 있다.
목을 스파이로 대체하기
스파이와 목의 차이점은 스파이는 직접 작성하는 반면, 목은 프레임워크의 도움을 받아 생성한다는 것이 유일한 차이이다. 시스템 끝에 있는 클래스에 대해서는 스파이가 목보다 낫다. 검증 단계에서 코드를 재사용해 테스트 크기가 줄고 가독성이 개선된다.
통합 테스트에만 목을 사용하고 단위 테스트에서는 하지 않기
목은 비관리 의존성만을 위한 것이고 이러한 의존성을 처리하는 코드는 컨트롤러뿐이므로 통합 테스트에서 컨트롤러를 테스트할 때만 목을 적용해야 한다. 단위 테스트에서는 목을 사용하지 말자.
테스트 당 목이 하나일 필요는 없음
테스트에서 사용되는 목의 수는 관계가 없다. 목의 수는 비관리 의존성의 수에 따라 달라진다.
호출 횟수 검증하기
목에 예상되는 호출이 있는지와 예상치 못한 호출이 없는지를 확인하라.
보유 타입만 목으로 처리하기
보유 타입만 목으로 처리하라. 비관리 의존성에 접근하는 서드파티 라이브러리 위에 어댑터를 작성하자. 기본 타입 대신 해당 어댑터를 목으로 처리하라.

Chap 10. 데이터베이스 테스트

통합 테스트에서 데이터베이스를 테스트하는 방법을 알아보자.
데이터베이스 테스트를 위한 전제 조건
데이터 베이스를 형상 관리 시스템이 유지
데이터베이스 스키마를 소스 코드와 같이 형상 관리 시스템에 저장하자. 테이블, 뷰, 인덱스, 저장 프로시저와 데이터베이스 구성 방식에 대한 청사진이 되는 기타 모든 항목 등이 데이터베이스 스키마에 해당된다.
참조 데이터도 데이터베이스 스키마다
참조 데이터도 데이터베이스 스키마에 해당한다. 이는 애플리케이션이 제대로 작동하도록 미리 채워져야 하는 데이터이다. 참조 데이터와 일반 데이터를 구별하려면 애플리케이션에서 해당 데이터를 수정할 수 있는지 확인하면 된다. 수정할 수 있으면 일반 데이터이고, 그렇지 않으면 참조 데이터이다.
모든 개발자를 위한 별도의 데이터베이스 인스턴스
개발자마다 데이터베이스 인스턴스를 별도로 두게 하라. 더 좋은 방법은 개발자 머신에 인스턴스를 호스팅하는 것인데, 이렇게 하면 테스트 실행 속도를 극대화 할 수 있다.
상태 기반 데이터베이스 배포와 마이그레이션 기반 데이터베이스 배포
상태 기반 데이터베이스 배포 방식은 상태를 명시적으로 만들고 비교 도구가 마이그레이션을 암묵적으로 제어할 수 있도록 한다. 마이그레이션 기반 방식은 데이터베이스를 특정 상태에서 다른 상태로 전환하게끔 명시적 마이그레이션을 사용하도록 한다. 데이터베이스 상태가 명확하면 병합 충돌을 좀 더 쉽게 처리할 수 있는데 반해, 명시적 마이그레이션은 데이터 모션 문제를 해결하는 데 도움이 된다.
데이터 모션 새로운 데이터베이스 스키마를 준수하도록 기존 데이터 형태를 변경하는 과정
데이터베이스 트랜잭션 관리
제품 코드에서 데이터베이스 트랜잭션 관리하기
비즈니스 연산은 데이터를 원자적으로 업데이트해야 한다. 원자성을 얻으려면 데이터베이스 트랜잭션 매커니즘을 의존하라. 또한, 가능하면 작업 단위 패턴을 사용하라. 작업 단위는 데이터베이스 트랜잭션에 의존하며, 비즈니스 연산 종료 시점까지 업데이트를 모두 지연시켜서 성능을 향상시킨다.
통합 테스트에서 데이터베이스 트랜잭션 관리하기
테스트 구절마다 데이터베이스 트랜잭션이나 작업 단위를 재사용하지 말라. 준비, 실행, 검증 구절에 각각 고유의 트랜잭션이나 작업 단위가 있어야 한다.
테스트 데이터 생명 주기
병렬 테스트 실행과 순차적 데이터 실행
통합 테스트는 순차적으로 실행하라. 병렬 실행에는 상당한 노력이 필요하며, 보통 그럴 가치가 없다.
테스트 실행 간 데이터 정리
테스트 시작 시점에 남은 데이터를 정리하라. 이 방식은 빠르고 일관성 없는 동작을 일으키지 않으며, 정리 단계를 실수로 건너뛰지 않는다. 이렇게 하면 별도의 종료 단계도 둘 필요가 없다.
인메모리 데이터베이스 피하기
SQLite와 같은 인메모리 데이터베이스는 사용하지 말라. 다른 업체의 데이터베이스로 테스트를 실행하면 보호 수준이 떨어진다. 테스트에서도 운영 환경과 같이 동일한 DBMS를 사용하라.
테스트 구절에서 코드 재사용하기
필수가 아닌 부분을 비공개 메서드 또는 헬퍼 클래스로 추출해 테스트를 단축하라
준비 구절에서는 테스트 데이터 빌더 대신 오브젝트 마더를 선택하라.
각 구절에 대해 별도의 DB 컨텍스트를 두고 난 후 통합 테스트 진행
실행 구절에서는 데코레이터 인터페이스를 도입하라.
어떤 컨트롤러 기능을 호출해야 하는지에 대한 정보가 있는 대리자를 받는 메서드를 도입할 수 있음.
검증 구절에서는 플루언트 인터페이스를 도입하라.
데이터베이스 테스트에 대한 일반적인 질문
읽기 테스트를 해야 하는가?
읽기 테스트 임계치는 쓰기 테스트 임계치보다 높아야 한다. 가장 복잡하거나 중요한 읽기 작업만 테스트하라. 나머지는 무시하라.
리포지터리 테스트를 해야 하는가?
리포지터리는 직접 테스트하지 말고 포괄적인 통합 테스트 스위트로 취급하라. 리포지터리 테스트는 회귀 방지에 대한 이득이 너무 적은 데 반해 유지비가 너무 높다.

단위 테스트 안티 패턴

Chap 11. 단위 테스트 안티 패턴

비공개 메서드 단위 테스트
비공개 메서드와 테스트 취약성
단위 테스트를 가능하게 하고자 비공개 메서드를 노출하게 되면 테스트가 구현에 결합되고, 결국 리팩터링 내성이 떨어진다. 비공개 메서드를 직접 테스트하는 대신, 식별할 수 있는 동작으로서 간접적으로 테스트하라.
비공개 메서드와 불필요한 커버리지
비공개 메서드가 너무 복잡하여 공개 API로 테스트할 수 없다면, 추상화가 누락됐다는 뜻이다. 비공개 메서드를 공개로 하지 말고 해당 추상화를 별도 클래스로 추출하라.
비공개 메서드 테스트가 타당한 경우
드물지만, 비공개 메서드가 클래스의 식별할 수 있는 동작에 속한 경우가 있다. 보통 클래스와 ORM(객체 관계 매핑)또는 팩토리 간의 비공개 계약을 구현하는 것이 여기에 해당한다.
비공개 상태 노출
비공개였던 상태를 단위 테스트만을 위해 노출하지 말라. 테스트는 제품 코드와 같은 방식으로 테스트 대상 시스템과 상호 작용해야 한다. 어떠한 특권도 가져서는 안된다.
테스트로 유출된 도메인 지식
테스트를 작성할 때 특정 구현을 암시하지 말라. 블랙박스 관점에서 제품 코드를 검증하라. 또한 도메인 지식을 테스트에 유출하지 않도록 하라
코드 오염
코드 오염은 테스트에만 필요한 제품 코드를 추가하는 것이다. 이는 테스트 코드와 제품 코드가 혼재되게 하고 제품 코드의 유지비를 증가시키기 때문에 안티 패턴이다.
구체 클래스를 목으로 처리하기
기능을 지키려고 구체 클래스를 목으로 처리해야 하면, 이는 단일 책임 원칙을 위반하는 결과다. 해당 클래스를 두 가지 클래스, 즉 도메인 로직이 있는 클래스와 프로세스 외부 의존성과 통신하는 클래스로 분리하라.
시간 처리하기
현재 시간을 앰비어트 컨텍스트로 하면 제품 코드가 오염되고 테스트하기가 더 어려워진다. 서비스나 일반 값의 명시적인 의존성으로 시간을 주입하라. 가능하면 항상 일반 값이 좋다.