⌜단위 테스트: 생산성과 품질을 위한 단위 테스트 원칙과 패턴⌟ - 블라디미르 코리코프
단위 테스트 : 네이버 도서
네이버 도서 상세정보를 제공합니다.
search.shopping.naver.com
1장: 단위 테스트의 목표
단위 테스트는 소프트웨어 프로젝트의 지속 가능한 성장을 목표로 한다. 좋은 단위 테스트를 작성하면 지속성과 확장성을 보장하고 장기적으로 개발 속도를 유지할 수 있다. 이외의 부가적인 (좋은) 사이드 이펙트로는 코드베이스를 점검하는 과정에서 더 좋은 소프트웨어 설계를 이룰 수 있다는 점이다.
좋은 테스트 작성/활용 방법
- 기반 코드 리팩토링 시 테스트 코드도 리팩토링하기
- 각 코드 변경 시 테스트 실행하기
- 테스트가 잘못된 경고(거짓 양성)를 발생시킬 시 처리하기
- 기반 코드 작동 원리를 이해하려고 할 때 테스트 코드를 읽는 데에 시간 투자하기
커버리지 지표
커버리지 지표의 정의와 종류
- 커버리지 지표란 테스트 수트가 소스 코드를 얼마나 실행하는지를 백분율로 나타낸 것이다. 커버리지는 좋은 부정 지표이지만 좋지 않은 긍정 지표이다. 즉 커버리지 지표가 낮다는 것은 소프트웨어 품질이 낮다는 것을 의미하지만, 커버리지 지표가 높다고 해서 테스트 수트의 품질이 좋다는 의미는 아니다.
- 코드 커버리지(테스트 커버리지)
- 코드 커버리지 = 실행 코드 라인 수 / 전체 라인 수
- 코드가 작을수록 코드 커버리지 지표는 더 좋아진다. 하지만 위에서 말했듯이 코드 라인 수 감소가 테스트 수트의 품질 개선으로 직결되지는 않는다.
- 분기 커버리지
- 분기 커버리지 = 통과 분기 / 전체 분기 수
- 원시 라인 수 대신 제어 구조에 중점을 두어 코드 커버리지의 단점을 보완했다.
커버리지 지표의 단점
- 테스트 대상 시스템(SUT; System Under Test)의 모든 가능한 결과를 검증한다고 보장할 수 없다.
- 외부 라이브러리의 코드 경로를 고려할 수 없다.
커버리지 지표를 활용하는 가장 좋은 방법은 지표 그 자체로 보는 것이며, 특정 커버리지 수치를 목표로 해서는 안 된다.
성공적인 테스트 수트의 특성
- 개발 주기에 통합되어 있다.
- 이상적으로는 코드 변경 시 아무리 작은 것이라도 테스트를 실행하는 것이다.
- 코드베이스에서 가장 중요한 부분만을 대상으로 한다.
- 비즈니스 로직에 집중한다.
- 최소 유지비로 최대 가치를 창출한다.
- 가치 있는 테스트를 식별하고, 가치 있는 테스트를 작성하는 방법을 알아야 한다.
2장: 단위 테스트란 무엇인가
단위 테스트는 작은 코드 조각(단위)을 검증하고, 빠르게 수행하고, 격리된 방식으로 처리하는 자동화된 테스트이다.
단위 테스트의 정의: 런던파와 고전파
- 단위 테스트를 정의할 때 격리 문제에 대한 관점이 두 가지 분파로 나뉘게 된다.
격리 문제에 대한 런던파의 접근
- 런던파에게 격리란 SUT를 협력자에게서 격리, 즉 의존성을 테스트 대역으로 대체한다는 것을 의미한다. 여기서 협력자란 SUT가 의존하는 대상을 의미한다.
- 런던파는 협력자를 목Mock(SUT와 협력자 간 상호작용을 검사할 수 있는 특별한 테스트 대역)으로 대체하여 테스트를 수행한다.
- 장점
- 이 방식대로 테스트를 수행하면 해당 테스트의 SUT가 코드베이스의 고장 부분임을 확실하게 알 수 있다.
- 객체 그래프(같은 문제를 해결하는 클래스 통신망)의 분할이 가능하다.
- 단점
- 협력자를 대체하기 위해 인터페이스가 필요하다.
- 각각의 클래스를 격리해야 하므로 단위(코드 조각)의 기준이 최대 클래스로 제한된다.
격리 문제에 대한 고전파의 접근
- 고전파에서는 격리 특성을 위해 단위 테스트들을 격리해 실행시킨다. 즉 여러 클래스가 모두 메모리에 상주하고 공유 상태에 도달하지 않으면 격리 상태로 본다.
- 고전파는 협력자 대체 없이 운영용 인스턴스를 사용해 테스트를 수행한다.
💡 의존성
- 공유 의존성: 테스트 간 공유되고 서로의 결과에 영향을 미칠 수 있는 수단을 제공하는 의존성이다. 예시로 정적 가변 필드, 데이터베이스 등이 있다.
- 비공개 의존성: 공유하지 않는 의존성이다.
- 프로세스 외부 의존성: 애플리케이션 실행 프로세스 외부에서 실행되는 의존성이며, 아직 메모리에 없는 데이터에 대한 프록시이다. 예시로 도커 컨테이너로 실행하는 데이터베이스 등이 있다.
- 고전파에서는 테스트 간 공유 상태를 일으키는 의존성에 대해서만 사용한다.
- 검증할 내용은 클래스가 아니라 단일 동작 단위이므로 단위(코드 조각)의 기준이 단일 클래스에만 국한되지 않는다.
런던파와 고전파의 비교
격리 주체 | 단위의 크기 | 테스트 대역 사용 대상 | |
런던파 | 단위 | 단일 클래스 | 불변 의존성 외 모든 의존성 |
고전파 | 단위 테스트 | 단일 클래스나 클래스 세트 | 공유 의존성 |
런던파의 장점 분석
런던파 방식대로 단위를 클래스별로 나누고, 불변 의존성 제외 모든 의존성을 목으로 대체하는 경우 아래와 같은 장점(?)이 있다.
- 입자성이 좋다. 한 번에 한 클래스만 테스트한다.
- → 테스트는 코드의 단위를 검증해서는 안 된다. 테스트의 목적은 특정 범위로 나누는 것이 아니라 동작의 단위를 검증해야 한다.
- 상호 연결된 클래스 그래프가 커져도 테스트하기 쉽다.
- → 큰 그래프를 가지지 않는 애플리케이션을 설계하는 것이 우선되어야 한다. 큰 그래프를 가지지 않는다면 고전파 방식대로 테스트해도 별 문제가 생기지 않는다.
- 버그 위치를 정확히 탐색할 수 있다.
- → 코드 수정 사항이 생길 때마다 테스트를 실행한다면 고전파 방식에서도 버그를 잘 추적할 수 있다.
- 또한 테스트가 SUT의 구현 세부 사항에 결합되는 과도한 명세overspecification 문제는 주로 고전파보다 런던파 방식에서 나타나기 쉽다.
두 분파의 통합 테스트
- 런던파: 실제 협력자 객체(목으로 대체하지 않는)를 사용하는 모든 테스트를 통합 테스트로 간주한다.
- 고전파: 공유・프로세스 외부 의존성에 접근해 격리되지 않거나 느리게 실행되는 테스트, 또는 여러 동작 단위를 검증하는 테스트를 통합 테스트로 간주한다.
- 엔드 투 엔드 테스트: 통합 테스트의 일부이며 모든 외부 애플리케이션을 포함해 시스템을 최종 사용자 입장에서 검증하는 테스트이다. 유지보수 측면에서 비용이 가장 많이 든다.
3장: 단위 테스트 구조
AAA 패턴
AAA 패턴이란 각 테스트를 준비(Arrange), 실행(Act), 검증(Assert) 세 과정으로 나누어 수행하는 테스트 구조를 말한다.
- 준비: SUT와 의존성을 원하는 상태로 만든다.
- 실행: SUT에서 메서드를 호출하고 준비된 의존성을 전달하며 출력을 캡처한다.
- 반환값・SUT나 협력자의 최종 상태・호출한 메서드 등의 결과를 검증한다.
- Given-When-Then 패턴은 AAA 패턴과 구조적으로는 동일하지만 비개발자가 이해하기 더 쉽다.
피해야 할 구조
- 여러 단계의 준비-실행-검증 과정
- 여러 단계의 과정은 여러 개의 동작 단위를 검증하는 테스트를 의미한다. 즉 더 이상 단위 테스트가 아니라 통합 테스트의 범주이다.
- 테스트 내부의 if문
- 단위/통합 테스트는 분기가 없는 일련의 단계여야 한다. 분기는 테스트가 한 번에 너무 많은 것을 검증한다는 표시이다.
각 구절의 크기
- 준비 구절
- 일반적으로 가장 크다. 실행과 검증 구절을 합한 것보다 클 수도 있지만, 이보다 너무 커지면 같은 클래스 내 비공개 메서드나 별도의 팩토리 메서드 클래스로 도출하는 것이 좋다. 도움되는 패턴으로 오브젝트 마더, 테스트 데이터 빌더가 있다.
- 실행 구절
- 일반적으로 한 줄이며 이보다 큰 경우 SUT의 공개 API에 문제가 있을 가능성이 크다. 실행하는 SUT API의 구조 자체를 검증해 보자.
- 호출 API가 2개 이상으로 나뉜다면(=실행 구절이 2줄 이상이라면) 하나의 동작을 위해 여러 메서드를 호출해야 한다는 뜻이다. 이는 불변 위반을 일으킬 수 있으며 이런 상황을 막기 위해 캡슐화를 항상 지켜야 한다. → 캡슐화를 따른다면 실행 구절도 한 줄로 줄어들 것이다.
- 검증 구절
- 단일 동작 단위는 여러 결과를 낼 수 있으므로 하나의 테스트에서 모든 결과를 검증해도 된다. 하지만 너무 커지는 것은 지양해야 하며, SUT 결과 객체의 모든 속성을 하나하나 비교하는 것보다 테스트 클래스에 동등 멤버를 정의해 비교하는 식으로 처리하는 것이 좋다.
테스트 픽스처
테스트 픽스처란 테스트 실행 대상 객체를 의미하며, 정규 의존성(SUT)에게 전달되는 인수이다. 각 테스트 실행 전에 알려진 고정 상태로 유지하기 때문에 동일한 결과를 생성한다.
- 테스트에서 언제 어떻게 코드를 재사용하는지 아는 것이 중요하다. 준비 과정에서 테스트 픽스처용 메서드・클래스를 사용해 도출한 뒤 테스트 간에 재사용하는 것이 좋다.
- 테스트 클래스에 비공개 팩토리 메서드를 두어 사용하는 것이 좋다. 공통 초기화 코드를 팩토리 메서드로 추출해 코드 중복을 제거한다. 테스트 진행 상황 맥락을 유지하면서 테스트끼리 결합되지 않고 사용할 수 있다.
4장: 좋은 단위 테스트의 4대 요소
1. 회귀 방지
회귀란 코드 수정 후 기능이 의도대로 작동하지 않는 상황을 의미한다. 즉 소프트웨어 버그를 말한다.
- 회귀 방지 지표는 아래 요소들에 의해 평가된다.
- 테스트 중 실행되는 코드의 양
- 코드 복잡도
- 코드 도메인 유의성
- 회귀 방지 지표를 극대화하기 위해서는 테스트가 가능한 많은 코드를 실행하도록 해야 한다.
2. 리팩토링 내성
리팩토링 내성은 테스트를 실패로 바꾸지 않고 애플리케이션 코드를 리팩토링할 수 있는지에 대한 척도이다.
- 기능은 올바르게 작동하지만 테스트가 실패하는 경우를 거짓 양성이라고 한다. 거짓 양성은 기능 고장 시 조기 경고(참 양성) 비율을 낮추고, 프로젝트에 대해 회귀가 생기지 않을 확신을 낮춘다.
- 거짓 양성은 테스트와 SUT의 구현 세부 사항이 많이 결합할수록 더 많이 발생한다. 세부 사항 대신 고객에게 유의미한 최종 결과를 검증하는 데 집중하자.
기능 작동 기능 고장 테스트 통과 올바른 추론(참 음성) 2종 오류(거짓 음성) 테스트 실패 1종 오류(거짓 양성) 올바른 추론(참 양성) - 거짓 음성을 피하기 위해 회귀 방지가 도움된다.
- 거짓 양성을 피하기 위해 리팩토링 내성이 도움된다.
- 테스트 정확도 = 발견된 버그 수 / 허위 경보 발생 수
- 회귀를 더 잘 찾아내는 테스트(분자 증가)로 개선하거나 허위 경보를 발생시키지 않는 테스트(분모 감소)로 개선해 테스트 정확도를 향상시킬 수 있다.
3. 빠른 피드백
빠른 피드백은 코드 결함이 생기자마자 버그에 대해 경고할 수 있는 것을 말한다.
4. 유지 보수성
유지 보수성은 단위 테스트를 얼마나 쉽게 유지 보수할 수 있는지를 말한다.
- 유지 보수성은 아래 요소들에 의해 평가된다.
- 테스트가 얼마나 이해하기 어려운지
- 테스트가 얼마나 실행하기 어려운지(의존성)
이상적인 테스트
- 단위 테스트의 4가지 특성을 모두 유지해야 가치있는 테스트이지만, 회귀 방지, 리팩토링 내성, 빠른 피드백은 서로 상호 배타적이므로 셋 중 하나를 너무 크게 훼손하지 않는 선에서 균형을 조절해야 한다.
- 리팩토링 내성은 거의 있거나/없거나 이분법적으로 나뉘므로 포기할 수 없다. 따라서 회귀 방지와 빠른 피드백 사이 적절한 포기를 하는 것이 절충안이다.
블랙박스 대 화이트박스 테스트
- 블랙박스 테스트는 시스템 내부구조를 몰라도 시스템 기능을 검사할 수 있는 테스트 방법이다. 어떻게 해야 하는지가 아니라 무엇을 해야 하는지에 초점을 맞춘다.
- 화이트박스 테스트는 내부 구조를 검증하는 테스트 방법이다. 요구사항이나 명세가 아닌 코드에서 테스트가 파생된다. 주로 더 철저한 편이고 테스트 대상 코드(Code Under Test)의 특정 구현과 결합되므로 깨지기 쉽다.
회귀 방지 | 리팩토링 내성 | |
화이트박스 | 좋음 | 나쁨 |
블랙박스 | 나쁨 | 좋음 |
- 리팩토링 내성은 타협할 수 없기 때문에 블랙박스를 기본 옵션으로 선택하자.
- 코드 커버리지 지표를 사용해 실행되지 않은 분기를 살피고(화이트박스 방식으로 분석), 코드 내부를 전혀 모르는 것처럼 테스트(블랙박스 방식으로 테스트)하는 것이 가장 좋다.
5장: 목과 스텁 구분
테스트 대역
- 테스트 대역은 모든 유형의 비운영용 가짜 의존성을 의미한다.
목
- 목은 외부로 나가는 상호작용을 모방/검사하는 데 도움되는 테스트 대역이다. 외부로 나가는 상호작용이란 SUT가 상태를 변경하기 위한 의존성을 호출하는 것을 의미한다.
- 목의 종류에는 목과 스파이가 있다.
- 예시로 이메일 발송은 SMTP 서버에 사이드 이펙트를 초래하는 상호작용이므로 목이 사용된다.
- 스텁은 내부로 들어오는 상호작용을 모방하는 데 도움되는 테스트 대역이다. 내부로 들어오는 상호작용이란 SUT가 입력 데이터를 얻기 위해 의존성을 호출하는 것을 의미한다.
- 스텁의 종류에는 스텁, 더미, 페이크가 있다.
- DB 데이터를 검색하는 것은 내부로 들어오는 상호작용이므로 사이드 이펙트가 없고, 스텁이 사용된다.
- 목은 SUT 관련 의존성 간의 상호작용을 모방하고 검사하지만, 스텁은 모방만 한다. 스텁은 입력을 제공하는 역할만 담당하므로 SUT-스텁 간 상호작용은 SUT의 최종 결과가 아닌 수단이다. 이 때 스텁과의 상호작용을 검사하는 것은 리팩토링 내성을 저하시킨다.
명령-조회 분리(CQS; Command Query Seperation)
- 모든 메서드는 명령이거나 조회여야 하며 둘을 혼용해서는 안 된다는 원칙이다.
- 명령: 사이드 이펙트 초래, 반환값 없음 → 목 사용
- 조회: 사이드 이펙트 없음, 값 반환 → 스텁 사용
- 추가적으로 메서드가 사이드 이펙트를 일으킨다면 반환 타입이 void인지 확인하는 것이 좋다.
- 예외로는 스택의 pop.() 등이 있다.
식별할 수 있는 동작과 구현 세부 사항
- 식별할 수 있는 동작이 되기 위해서는 다음 중 하나를 해야 한다.
- 클라이언트가 목표를 달성하는 데 도움이 되는 연산 노출
- 클라이언트가 목표를 달성하는 데 도움이 되는 상태 노출
- 구현 세부사항은 둘 다 하지 않는다.
- 이상적으로는 시스템 공개 API가 식별할 수 있는 동작과 일치하고, 모든 구현 세부사항은 비공개 API 뒤에서 클라이언트에게 보이지 않아야 한다.
- 구현 세부사항을 숨기면 클라이언트 시야에서 클래스 내부를 가릴 수 있어 손상 위험이 적다.
- 데이터와 연산을 결합하면 해당 연산이 클래스 불변성을 위반하지 않도록 할 수 있다.
목과 테스트 취약성 간의 관계
육각형 아키텍처
- 육각형 아키텍처는 도메인 계층과 애플리케이션 서비스 계층으로 나뉘어진 애플리케이션 구조를 말한다. 도메인은 비즈니스 로직을 포함하고 애플리케이션 서비스는 도메인 위에 있으면서 외부 환경과의 통신을 조정한다.
- 육각형 계층이 강조하는 지침
- 도메인 계층과 애플리케이션 서비스 계층의 관심사 분리: 도메인은 비즈니스 로직에 대해서만 책임을 진다.
- 애플리케이션 내부 통신: 애플리케이션 서비스 → 도메인 방향으로 흐르는 단방향 의존성 흐름을 규정한다.
- 연산 수행을 위한 도메인 클래스 간의 협력은 식별 불가능하므로 구현 세부사항에 속한다.
- 애플리케이션 간의 통신: 외부 애플리케이션은 애플리케이션 서비스의 공통 인터페이스를 통해 연결된다.
- 시스템 외부 환경과 통신하므로 식별할 수 있는 동작에 속한다.
- 목은 외부 환경에 사이드 이펙트를 발생시키는 호출(시스템 간 통신)에 사용하고 애플리케이션 내부에서의 통신에는 사용하지 않는다.
- 런던파는 불변 의존성 외 모든 의존성을 목으로 대체한다. 즉 시스템 내부 통신과 시스템 간 통신을 구분하지 않는다.
- 모든 프로세스 외부 의존성을 목으로 해야 하는 것은 아니다. 프로세스 외부 의존성이 애플리케이션을 통해서만 접근할 수 있으면 해당 의존성은 애플리케이션의 일부로 작용하므로 구현 세부 사항이다.
- 목은 애플리케이션의 경계를 넘나드는 상호 작용(시스템 간 통신)을 검증할 때와 이러한 상호작용의 사이드 이펙트가 외부 환경에서 보일 때만 동작과 관련된다.
6장: 단위 테스트 스타일
테스트 스타일
- 출력 기반 테스트: SUT에 입력을 넣고 생성되는 출력을 점검한다. 전역 상태나 내부 상태를 변경하는 코드가 없는 경우에만 적용 가능하다. 출력값만 검증한다.
- 상태 기반 테스트: 작업 완료 후 시스템 상태(SUT, 협력자, 프로세스 외부 의존성 등의 상태)를 확인한다.
- 통신 기반 테스트: 협력자를 목으로 대체해 SUT와의 통신을 검증한다.
- 테스트 품질은 출력 > 상태 > 통신 순이다. 출력 기반 테스트는 함수 기반 아키텍처에만 적용된다.
테스트 스타일 비교
1. 리팩토링 내성 관점
- 식별할 수 있는 동작이 아니라 구현 세부 사항에 결합될수록 리팩토링 내성이 낮다.
- 출력 기반: 테스트가 테스트 대상 메서드에만 결합된다.
- 상태 기반: API 호출 영역에 의존하므로 구현 세부 사항과 결합될 가능성이 출력 기반보다 높아진다.
- 통신 기반: 테스트 대역으로 상호작용을 검증하는 경우 테스트가 깨지기 쉽다. 항상 스텁과 상호작용하는 경우이므로 이런 상호작용을 확인해서는 안 된다.
2. 유지 보수성 관점
- 테스트 이해 난이도(테스트 크기), 테스트 실행 난이도(테스트의 프로세스 외부 의존성 개수)가 높을수록 유지보수성이 낮다.
- 출력 기반: 전역 상태, 내부 상태 변경이 없으므로 프로세스 외부 의존성을 다루지 않는다.
- 상태 기반: 주로 상태 검증이 출력 검증보다 더 많은 공간을 차지한다. 헬퍼 메서드나 동등 멤버 정의로 완화 가능하다.
- 동등 멤버 비교는 클래스가 값에 해당하고 값 객체로 변환할 수 있을 때만 효과적이다.
- 통신 기반: 테스트 대역 상호 작용 검증에서 공간을 더 많이 차지한다. 목 체인이 존재하는 경우 테스트가 더 커진다.
- 결론적으로 출력 기반 테스트가 가장 좋다.
함수형 아키텍처 이해
- 함수형 프로그래밍은 숨은 입력이 없는 수학적 함수(메서드)를 사용하는 프로그래밍이다.
- 숨은 입출력 유형
- 사이드 이펙트: 메서드 시그니처에 표시되지 않은 출력, 클래스 인스턴스의 상태 변경 등
- 예외: 메서드 시그니처에 설정된 계약 우회, 호출 스택의 어느 곳에서나 발생할 수 있음 → 출력 추가
- 내외부 상태에 대한 참조: 메서드 시그니처에 없는 실행 흐름에 대한 입력
- 함수형 프로그래밍의 목표는 비즈니스 로직을 처리하는 코드와 사이드 이펙트를 발생시키는 코드를 분리하는 것이다.
함수형 아키텍처
- 함수형 아키텍처란 사이드 이펙트를 다루는 코드를 최소화하면서 순수 함수(불변) 방식으로 작성된 코드를 극대화하는 아키텍처이다.
- 결정을 내리는 코드: 사이드 이펙트가 없으므로 수학적 함수 사용 가능 → 함수형 코어Functional Core
- 해당 결정에 따라 작동하는 코드: 결정을 가시적 부분으로 변환 → 가변 셸Mutable Shell
- 함수형 코어와 가변 셸의 협력 방식
- 가변 셸이 모든 입력 수집
- 함수형 코어가 결정 생성
- 가변 셸이 결정을 사이드 이펙트로 변환
함수형 아키텍처와 육각형 아키텍처
- 유사점: 계층 분리, 의존성 간 단방향 흐름
- 차이점: 함수형은 사이드 이펙트를 가변 셸이 처리, 육각형은 도메인 계층 내부에서 처리
함수형 아키텍처와 출력 기반 테스트로의 전환
- 리팩토링 단계
- 프로세스 외부 의존성에서 목으로 변경
- 목에서 함수형 아키텍처로 변경
- 함수형 아키텍처의 단점: 결정 이전에 모든 입력이 수집되지 않고 중간 결과에 따라 프로세스 외부 의존성이 추가 데이터를 질의하는 경우 사용 불가능, 성능 저하, 코드베이스 크기 증가
7장: 가치 있는 단위 테스트를 위한 리팩토링
리팩토링할 코드 식별하기
- 모든 제품 코드는 다음 2개의 차원으로 분리할 수 있다.
- 복잡도 또는 도메인 유의성
- 협력자 수
- 코드 복잡도는 코드 내 의사결정(분기) 지점 수로 정의된다.
- 도메인 유의성은 코드가 프로젝트 문제 도메인에 대해 얼마나 의미있는지를 나타낸다.
- 협력자(가변 의존성/프로세스 외부 의존성)가 많은 코드는 테스트 비용이 증가한다.
복잡도 및 도메인 유의성 높음 A: 도메인 모델 및 알고리즘 D: 지나치게 복잡한 코드 복잡도 및 도메인 유의성 낮음 B: 간단한 코드 C: 컨트롤러 협력자 수 적음 협력자 수 많음 - A 도메인 모델 및 알고리즘: 도메인 모델/문제 도메인과는 직접적 관련이 없지만 복잡한 알고리즘 코드
- B간단한 코드: 기본 생성자와 한 줄 속성 등 협력자가 없고 복잡도/도메인 유의성도 없는 코드
- C컨트롤러: 비즈니스적 중요 작업이 아니라 도메인 클래스와 외부 애플리케이션 등 다른 구성 요소 작업을 조정하는 코드
- D지나치게 복잡한 코드: 덩치가 큰 컨트롤러(위임 없이 스스로 작업하는 컨트롤러) 등의 코드
- A 테스트 시 노력 대비 가장 이롭다. 회귀 방지와 테스트 유지비도 감소한다.
- B는 테스트의 가치가 없다.
- C는 통합 테스트의 일부로서 간단하게 테스트한다.
- D는 테스트가 어려우므로 A와 C로 분리하는 작업이 필요하다.
가치있는 단위 테스트를 위한 리팩토링하기
- 1단계: 암시적 의존성을 명시적 의존성으로 만들기
- 암시적 의존성에 대한 인터페이스를 두고 SUT에 주입한 다음 테스트에서 목으로 처리
- 2단계: 애플리케이션 서비스 계층 도입
- 도메인 모델이 외부 시스템과 직접 통신하지 않도록 험블 컨트롤러(애플리케이션 서비스 계층) 클래스로 책임 전가
- 3단계: 애플리케이션 서비스 복잡도 낮추기
- ORM을 사용해 데이터베이스를 도메인 모델에 매핑하거나 도메인 모델에 원시 데이터베이스 데이터로 도메인 클래스를 인스턴스화하는 팩토리 클래스 작성
- 4단계: 새 클래스 생성
- 클래스 하나로 담당하는 조직이 어색한 경우 데이터를 함께 묶는 새 클래스를 만들어 사용
최적의 단위 테스트 커버리지 분석
- 도메인 계층(A)과 유틸리티 코드: 분기별로 테스트 수행, 매개변수화된 테스트 사용 가능
- B 사분면: 단순한 테스트로 회귀 방지
컨트롤러에서 조건부 로직 처리
- 비즈니스 로직과 오케스트레이션의 분리는 연산이 ‘저장소에서 데이터 검색 → 비즈니스 로직 실행 → 데이터를 다시 저장소에 저장’의 세 단계일 때 가장 효과적이다.
- 위처럼 간단한 구조가 아닐 때 아래와 같은 방법을 사용한다.
- 외부에 대한 모든 읽기/쓰기를 가장자리로 밀어내기
- 도메인 모델에 외부 의존성을 주입하고 비즈니스 로직이 해당 의존성을 호출할 시점을 직접 결정
- 의사결정 프로세스 단계 세분화, 각 단계별 컨트롤러 실행
- ‘도메인 모델 테스트 용이성’, ‘컨트롤러 단순성’, ‘성능’ 세 가지의 균형을 잘 맞추는 것이 중요하다.
CanExecute/Execute 패턴
- CanExecute/Execute 패턴을 이용해 비즈니스 로직이 도메인 모델에서 컨트롤러로 유출되는 것을 방지할 수 있다.
- 클래스에 특정 행동을 할 수 있는지 확인하는 CanExecute 메서드를 두고, 클래스 멤버에 해당 결정을 담당하는 플래그 속성을 추가한다. Execute 메서드를 실행하는 전제조건으로 해당 플래그를 확인한다.
- 이 패턴을 사용하면 컨트롤러는 더 이상 프로세스를 알 필요가 없다.
'공부' 카테고리의 다른 글
[육각형 아키텍처] 데이터 기본값은 어디에서 세팅해야 할까? (0) | 2025.02.10 |
---|---|
[육각형 아키텍처] 육각형 아키텍처 적용기 (0) | 2025.02.09 |
Gradle 의존성(Dependencies) 주요 공식문서 정리 (0) | 2025.02.06 |
[단위 테스트] JaCoCo + CI Github Actions 배지 생성하기 (0) | 2025.02.06 |