티스토리 뷰

책도 읽고

단위테스트 4-7장

eungding 2023. 2. 20. 18:43
반응형

단위 테스트 4-7장 (Part 2. 개발자에게 도움이 되는 테스트 만들기) 내용 정리


 

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

 

# 좋은 단위 테스트의 네 가지 특성 

- 회귀 방지 

- 리팩터링 내성

- 빠른 피드백

- 유지 보수성

 

1) 첫번째 요소: 회귀 방지

회귀 방지는 테스트가 얼마나 버그(회귀)의 존재를 잘 나타내는 지에 대한 척도다. 

회귀 방지 지표에 대한 테스트 점수가 얼마나 잘 나오는 지 평가하려면 

 

테스트 중에 실행되는 코드의 양 / 코드 복잡도 / 코드의 도메인 유의성 을 고려해라. 

 

단순한 코드를 다루는 테스트는 회귀 오류가 많이 생기지 않는다.

기반 코드에 실수할 여지가 많지 않다면 테스트는 회귀를 나타내지 않을 것이기 때문 이다. 

복잡한 비즈니스로직을 다루는 테스트가 더 좋다. 

 

 

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

리팩터링 내성은 테스트가 거짓 양성을 내지않고 애플리케이션 코드 리팩터링을 유지할 수 있는 정도를 의미한다. 

 

✓ 거짓 양성 (허위 경보)

= 리팩토링 후, 기능이 의도한대로 동작하지만(또는 동작이 변하지 않았지만) 테스트가 깨지는 경우

 

기능이 제대로 작동하지 않으면 테스트가 실패하는 것은 단위테스트의 핵심이다. // 참 양성

하지만 기능이 올바르지만 테스트가 실패하는 경우가 있다. // 거짓 양성

 

✓ 거짓 양성의 위험성 

테스트가 타당한 이유없이 실패하면 실패에 익숙해지고 그만큼 신경을 많이 쓰지 않는다. 

타당한 실패 (기능이 동작X)도 무시하기 시작해 기능이 고장나도 운영환경에 들어가게 된다.

 

거짓양성이 빈번하면 테스트 스위트에 대한 신뢰가 서서히 떨어지면서 안전망으로 인식하지 않는다. 

리팩터링이 줄어든다. 회귀를 피하려고 코드 변경을 최소한으로 하기 때문이다. 

 

✓ 거짓 양성의 원인 / 줄이는 방법 

거짓양성은 테스트와 SUT의 구현 세부사항 간의 강결합의 결과다. 

결합도를 낮추려면 테스트는 SUT가 수행한 단계가 아니라 SUT가 만든 최종결과를 검증해야한다. 

 

허위 경보를 많이 주지 않으려면 해당 구현세부사항에서 테스트를 분리하라 

테스트를 통해 SUT가 제공하는 최종결과(관련된 절차가 아니라 식별할 수 있는 동작)을 검증하라 

 

식별할 수 있는 동작 

- 클라이언트가 목표를 달성하는데 도움이 되는 동작 

- 해당 클라이언트가 누구 인지 해당 클라이언트의 목표가 무엇인지에 달려있음 

 

정리하자면..

SUT의 구현 세부사항과 결합된 테스트는 리팩터링 내성이 없다.

 

구현세부사항과 테스트간의 결합도를 낮춰라

구현 세부사항 대신 최종 결과 (식별할 수 있는 동작) 를 목표로 하라

 

 

3) 세 번째 요소: 빠른 피드백

빠른 피드백은 테스트가 얼마나 빨리 실행되는 지에 대한 척도다. 

테스트 속도가 빠를 수록 테스트 스위트에서 더 많은 테스트를 수행할 수 있고 더 자주 실행할 수 있다. (단위 테스트의 필수 속성)

 

 

4) 네번째 요소: 유지보수성 

유지비는 두 가지 요소로 구성된다.

 

- 테스트 이해 난이도

테스트의 크기가 작을 수록 읽기 쉽니다. 

 

- 테스트 실행 난이도

외부 의존성이 적을수록 쉽게 운영할 수 있다. 

 

 

# 테스트의 가치 추정

테스트 가치는 네 가지 특성 각각에서 얻은 점수의 곱.

특성 중 하나더라도 0이면 테스트 가치도 0이 된다. 

 

리팩터링 내성은 다른 특성과 달리 대부분 이진선택이다. (테스트에 리팩터링 내성이 있거나 아예없다)

 

 

# 테스트 피라미드 

테스트 피라미드는 테스트 유형 (단위테스트 - 통합테스트 - 엔드투엔드 테스트) 간의 일정한 비율을 나타낸다. 

 

층의 너비 - 테스트 스위트 수

층의 높이 - 사용자 모방 (최종 사용자의 동작을 얼마나 유사하게 흉내 내는지) 

 

엔드 투 엔드 테스트는 회귀 방지를 선호

단위테스트는 빠른 피드백을 선호 

 

 

# 블랙박스, 화이트 박스 

블랙박스 - 어떻게 해야하는 지가 아니라 무엇을 해야하는 지 검증

화이트 박스 - 정반대. 테스트는 요구사항이나 명세가 아닌 소스코드에서 파생된다. 

 

테스트를 작성할 때는 블랙박스 테스트 방법이 좋지만

테스트를 분석할 때는 화이트 박스 방법을 사용할 수 있다. 

 


5장 목과 테스트 취약성 

 

# 목과 스텁 구분 

목 - 외부로 나가는 상호작용을 모방 (SUT에서 의존성으로의 호출로, 해당 의존성의 상태를 변경)

스텁 - 내부로 들어오는 상호작용을 모방 (SUT가 해당 의존성을 호출해 입력 데이터를 가져옴)

 

ex. 이메일 발송은 SMTP 서버에 부작용을 초래하는 상호 작용. 즉 외부로 나가는 상호작용. 목은 이러한 상호 작용을 모방하는 테스트 대역에 해당함.

데이터베이스에서 데이터를 검색하는 것은 내부로 들어오는 상호 작용. 부작용을 일으키지 X. 해당 테스트 대역은 스텁

 

# 테스트 대역 5가지 유형 

테스트 대역은 목과 스텁의 두가지 유형으로 나눌 수 있다.

 

✓ 목 (목, 스파이)

스파이 - 직접 작성한 목 // 목은 목 프레임워크의 도움을 받아 생성. 스파이는 수동으로 작성

 

✓ 스텁 (스텁, 더미, 페이크) 

세개의 차이는 얼마나 똑똑한 지에 있다. 

 

더미 - null값, 가짜 문자열과 같이 단순하고 하드코딩된 값. SUT의 메소드 시그니처를 만족시키기 위해 사용하고 최종 결과를 만드는데 영향을 주지 않음. 

스텁 - 더 정교하다. 시나리오마다 다른 값을 반환하게끔 구성할 수 있도록 필요한 것을 다 갖춘 완전한 의존성

페이크 - 아직 존재하지 않는 의존성을 대체하고 자 구현 

 

 

# 스텁으로 상호작용을 검증하지 말라

스텁과의 상호 작용을 검증하는 것은 취약한 테스트를 야기하는 일반적인 안티패턴이다. 

4장에서 본 것 처럼 구현세부사항이 아니라 최종결과를 검증해야 리팩터링 내성이 높아지기 때문.

 

mock.Verify(x => x.SendGreetingsEmail("user@gmail.com")

 

예제 5.1에서 위 구문은 실제 결과에 부합하며 해당 결과는 도메인 전문가에게 의미가 있다. 

 

반면  예제 5.2 에서 GetNumberOfUsers() 를 호출하는 것은 전혀 결과가 아니다. 이는 SUT가 보고서 작성에 필요한 데이터를 수집하는 방법에 대한 내부 구현 세부사항이다. 이러한 호출을 검증하는 것은 테스트 취약성으로 이어질 수 있다. 

 

최종 결과가 아닌 사항을 검증하는 이런 관행을 과잉명세 (overspecification) 라고 부른다. 

 

 

# 목과 스텁은 명령과 조회에 어떻게 관련돼 있는가?

명령을 대체하는 테스트 대역은 목

조회를 대체하는 테스트 대역은 스텁

 

 

# 육각형 아키텍쳐

- 도메인 계층과 애플리케이션 서비스 계층의 관심사 분리

- 애플리케이션 내부통신  // 애플리케이션 -> 도메인으로 흐르는 단방향 의존성 흐름을 규정

- 애플리케이션 간의 통신  // 외부 애플리케이션은 애플리케이션 서비스 계층에 있는 공통 인터페이스를 통해 해당 애플리케이션에 연결된다. 아무도 도메인 계층에 직접 접근할 수 없다. 육각형이라고 애플리케이션이 다른 애플리케이션을 여섯 개까지만 연결할 수 있는 것은 아님.  연결의 수는 임의로 정할 수 있음 

 

 

# 육각형 아키텍쳐 > 테스트 취약성 

두가지 통신 유형 중, 내부 통신(도메인 클래스 간의 협력)은 식별할 수 있는 동작이 아니므로 구현 세부사항에 해당된다.

이러한 협력은 클라이언트 목표와 직접적인 관계가 없다. 이러한 협력과 결합하면 테스트가 취약해진다. 

 

 

 

예를들어..

CustomerController 클래스는 도메인 클래스 (Customner, Product, Store) 와 외부 애플리케이션 (SMTP 서비스의 프록시인 EmailGateWay) 간의 작업을 조정하는 애플리케이션 서비스.

 

클라이언트의 구매 목표와 관련있는 것은 두 메소드 뿐. 

customer.Purchase()  // 구매를 시작 

store.GetInventory()  // 구매가 완료된 후 시스템 상태를 보여줌

 

RemoveInventory() 메서드 호출은 고객의 목표로가는 중간단계(구현 세부사항) 에 해당함. 

 

------

목표를 달성하고자 각 개별 클래스가 이웃 클래스와 소통하는 방식은 식별할 수 있는 동작과 아무런 관계가 없다. 

이러한 세부 수준은 너무 세밀하다. 중요한 것은 클라이언트 목표로 거슬러올라갈 수 있는 동작이다. 

 

 

 

 

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

런던파는 불변 의존성을 제외한 모든 의존성에 목사용을 권장하며 시스템 내 통신, 시스템 간 통신을 구분하지 않는다. 

그 결과, 테스트는 애플리케이션과 외부 시스템 간의 통신을 확인하는 것처럼 클래스 간 통신도 확인한다.

런던파를 따라 목을 무분별하게 사용하면 종종 구현세부사항에 결합돼 테스트에 리팩터링 내성이 없게 된다.

 

반면 고전파는 테스트간에 공유하는 의존성(대부분이 SMTP 서비스나 메세지 버스 등 프로세스 외부 의존성에 해당) 만 교체하자고 하므로 이 문제에 훨씬 유리하다. 

 

 

# 모든 프로세스 외부 의존성을 목으로 해야하는 것은 아니다 

 

프로세스 외부 의존성이 애플리케이션을 통해서만 접근할 수 있으면 시스템에서 식별할 수 있는 동작이 아니다. 구현 세부사항이다. 

 

외부에서 관찰할 수 없는 프로세스 외부 의존성은 애플리케이션의 일부로 작용한다.  

같이 배포하기 때문에 클라이언트에 영향을 미치지도 않는다.

 

좋은 예로 애플리케이션 데이터 베이스 (애플리케이션에서만 사용되는 데이터 베이스) 가 있다. 

클라이언트 시야에서 완전히 숨어있기 때문에 전혀 다른 저장 방식으로 대체할 수 있다. 

 

이렇게 완전한 통제권을 가진 프로세스 외부 의존성에 목을 사용하면 깨지기 쉬운 테스트로 이어진다.

데이터베이스와 애플리케이션은 하나의 시스템으로 취급해야한다.

 

하지만 피드백 속도(좋은 단위테스트의 세번째 특성) 를 저하시킨다는 문제가 있다. 6,7장에서 이 주제를 자세히 보자

 

 

# 요약

시스템 내 통신을 검증하고자 목을 사용하면 취약한 테스트로 이어진다.

시스템 간 통신 (애플리케이션 경계를 넘는 통신)과 해당 통신의 부작용이 외부환경에서 보일 때만 목을 사용하는 것이 타당하다. 

 

 


6장 단위 테스트 스타일

 

# 단위테스트 스타일 

 

✓ 출력기반 output-based testing

SUT에 입력을 주고 출력을 확인하는 테스트 스타일. 이 테스트 스타일은 숨은 입출력이 없다고 가정하고, SUT 작업의 결과는 반환하는 값 뿐이다. 

 

✓상태기반 state-based testing

작업이 완료된 후 시스템의 상태를 확인한다.

 

✓ 통신기반 communication-based testing

목을 사용해서 SUT와 협력자 간의 통신을 검증한다. 

 

 

출력 기반 테스트가 가장 테스트 품질이 좋다.

구현 세부 사항에 거의 결합되지 않으므로 리팩토링 내성이 있다. 작고 간결해서 유지보수 하기 쉽다. 

 

 

# 함수형 아키텍쳐 

부작용을 비즈니스 연산 끝으로 몰아서 비즈니스 로직을 부작용과 분리한다. 

두가지 코드 유형(비즈니스 로직을 처리하는 코드 / 부작용을 일으키는 코드) 를 구분한다. 

 

- 함수형 코어: 결정을 내리는 코드

- 가변셀: 입력 데이터를 함수형 코어에 공급하고 코어가 내린 결정을 부작용으로 변환한다. 

 

 

1. 가변 셀은 모든 입력을 수집한다

2. 함수형 코어는 결정을 생성한다

3. 셀은 결정을 부작용으로 변환한다. 

 

 

출력기반 테스트로 함수형 코어를 두루 다루고 가변 셸을 훨씬 더 적은 수의 통합테스트의 맡기는 것이 좋다. 

 

 

# 함수형 아키텍쳐와 육각형 아키텍쳐의 비교

 

✓ 공통점 

관심사 분리, 의존성 간 단방향흐름

육각형 아키텍쳐 - 도메인 계층 내 클래스는 서로에게만 의존 (애플리케이션 서비스 계층 클래스에 의존 X)

함수형 아키텍쳐 - 불변코어는 가변셀에 의존하지 않는다

 

✓ 차이점

부작용에 대한 처리

함수형 아키텍처 - 모든 부작용을 불변 코어에서 비즈니스 연산 가장자리 (가변 셸) 로 밀어낸다.

육각형 아키텍쳐 - 도메인 계층에만 한정돼 있는 한은 도메인 계층에 의해 만들어진 부작용도 괜찮다.

 

 

# 함수형 아키텍쳐와 출력 기반 테스트로 전환

 

감사 시스템을 함수형 아키텍쳐로 리팩토링+ 출력기반 테스트 하는 예제  goooooood! 

 

 

FileContent를 받고 FileUpdate 명령을 리턴해주는 함수형 코어를 만듦.

ApplicationService를 만들어서 함수형 코어와 가변셸을 붙이면서 외부클라이언트를 위한 시스템의 진입점 제공.

육각형 아키텍쳐에서는 ApplicationService, Persister는 애플리케이션 서비스 계층, AuditManager는 도메인 모델에 속하는 셈. 

 

그러면 초기버전 과 달리 목없이 출력기반 테스트를 진행할 수 있다. 

 

 

모든 코드베이스를 함수형 아키텍쳐로 전환할 수 는 없다. 함수형 아키텍쳐를 전략적으로 적용하라

 


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

 

# 네가지 코드유형 

 

 

 

✓ 도메인 모델 및 알고리즘

복잡한 코드 (코드 내 분기 수)와 도메인 유의성(코드가 프로젝트 문제 도메인에 대해 얼마나 의미 있는 지)을 갖는 코드가 단위테스트에서 가장 이롭다.  // 일반적으로 도메인 계층의 모든 코드는 최종사용자의 목표와 직접적인 연관성이 있으므로 도메인 유의성이 높다.

 

해당 코드가 복잡하거나 중요한 로직을 수행해서 테스트의 회귀방지가 향상되기 때문에 가치가 있다.

또한 코드에 협력자가 거의 없어서 테스트 유지비를 낮추기 때문에 저렴하다. 

 

✓ 간단한 코드

테스트할 가치가 전혀 없다

 

✓ 컨트롤러

통합 테스트를 통해 간단히 테스트해야한다.

 

✓ 지나치게 복잡한 코드

가장 문제가 되는 코드 유형

도메인 모델 및 알고리즘 / 컨트롤러로 분할해야한다. 

 

 

이상적으로는 우측상단 사분면에 속하는 코드가 있으면 안된다. 

 

 

 

# 육각형, 함수형 아키텍쳐에서 4가지 코드 유형

 

 

 

# 험블 객체 (Humble Object ) 패턴

 

지나치게 복잡한 코드를 쪼개려면 험블 객체 패턴을 써야한다.

- 비즈니스 로직을 별도의 클래스로 추출해서 복잡한 코드 테스트

- 나머지 코드는 비즈니스 로직을 둘러싼 얇은 험블 래퍼 (컨트롤러) 가 됨 

 

험블 래퍼는 테스트하기 어려운 의존성과 로직을 붙이지만, 자체적인 로직이 거의 없거나 전혀 없으므로 테스트할 필요가 없다. 

사실 육각형 아키텍쳐, 함수형 아키텍쳐는 모두 정확히 이 패턴을 구현한다. 

다른 예로 MVP의 프리젠터, MVC의 컨트롤러는 험블 객체로 뷰와 모델을 붙인다.

 

코드의 깊이와 너비 관점에서 비즈니스 로직과 오케스트레이션 책임을 생각하라. 

코드가 깊거나(복잡하거나 중요함) 넓을(많은 협력자와 작동함) 수 있지만, 둘 다 가능하지는 않다. 

 

비즈니스 로직과 오케스트레이션을 계속 분리해야하는 이유는 테스트 용이성이 좋아지고 코드복잡도를 해결할 수 있으며 프로젝트 성장에도 중요한 역할을 하기 때문이다.

 

 

# 가치 있는 단위테스트를 위한 리팩터링하기 

고객 관리 시스템 리팩토링 예제  goooooood! 

 

 

- 험블 컨트롤러 (UserController)

 

- Database와 MessageBus는 프로세스 외부 협력자. 도메인 유의성이 높은 코드에서 프로세스 외부협력자는 사용하면 안된다. 

 

- 전제 조건 (ex. precondition) 을 테스트해야하는가? 전제조건테스트가 테스트 스위트에 있을 만큼 충분히 가치가 있는가?

일반적으로 권장하는 지침은 도메인 유의성이 있는 한 모든 전제 조건을 테스트 하라는 것이다. 

(ex. Company > 직원 수가 음수가 되면 안된다는 요구사항)  -> 테스트 0

(ex. UserFactory > data 의 length가 3 이상 이여야한다는 보호장치)  -> 테스트 X

 

 

✓ CanExecute / Execute 패턴

비즈니스 로직이 도메인 모델에서 컨트롤러로 유출되는 것을 방지할 수 있다.

각 Do() 메서드에 대해 CanDo() 를 두고, CanDo() 가 성공적으로 실행되는 것을 Do() 의 전제 조건으로 한다. 

 

✓ 도메인 이벤트  

도메인 이벤트는 컨트롤러에서 의사 결정 책임을 제거하고 해당 책임을 도메인 모델에 적용한다.

그로 인해 컨트롤러를 검증하고 프로세스 외부 의존성을 목으로 대체하는 대신, 단위테스트에서 직접 도메인 이벤트 생성을 테스트할 수 있다. 이 테스트는 간결하다. 

 

도메인 이벤트는 이미 일어난 일들을 나타내기 때문에 항상 과거 시제로 명명해야한다. ( ex. EmailChangedEvent) 

 

 

 

 

 

반응형
댓글