5장 목과 테스트 취약성
날짜
2024년 4월 11일
런던파 : 테스트 대상 코드 조각을 서로 분리하고 불변 의존성을 제외한 모든 의존성에 테스트 대역을 써서 격리하자.
고전파 : 단위 테스트를 분리해서 병렬로 실행할 수 있게 하자. 테스트 간에 공유하는 의존성에 대해서만 테스트 대역을 사용하자.
목과 스텁 구분
테스트 대역 유형
목은 외부로 나가는 상호 작용을 모방하고 검사하는 데 도움이 된다. 이러한 상호 작용은 SUT가 상태를 변경하기 위한 의존성을 호출하는 것에 해당한다.
스텁은 내부로 들어오는 상호 작용을 모방하는 데 도움이 된다. 이러한 상호 작용은 SUT가 입력 데이터를 얻기 위한 의존성을 호출하는 것에 해당한다
Mock은 외부로 나가는 상호 작용을 모방하고 검사하는데 도움이 된다.
Stub은 내부로 들어오는 상호 작용을 모방하는데 도움이 된다.
테스트 대역 다섯 가지 변형의 차이점
목(mock)
스파이 : 수동으로 작성 (목과 동일한 역할을 함)
목 : 목 프레임워크의 도움을 받음
스텁(stub)
더미 : 단순하고 하드코딩된 값
스텁 : 시나리오마다 다른 값을 반환하게끔 구성할 수 있도록 필요한 것을 다 갖춘 완전한 의존성
페이크 : 대다수의 목적에 부합하는 스텁과 같으나 보통 아직 존재하지 않는 의존성을 대체하고자 구현한다.
도구로서의 목과 테스트 대역으로서의 목
Mock은 두가지 의미가 있다.
목 라이브러리(mocking library)의 클래스
테스트 대역
스텁으로 상호 작용을 검증하지 말라
SUT에서 스텁으로의 호출은 SUT가 생성하는 최종 결과가 아니라 최종 결과를 산출하기 위한 수단일 뿐이다.
스텁과의 상호 작용을 검증하는 것은 취약한 테스트를 야기하는 일반적인 안티 패턴이다.
목과 스텁은 명령과 조회에 어떻게 관련돼 있는가?
명령 조회 분리(CQS) 원칙에서 명령은 목에 해당하는 반면, 조회는 스텁과 일치한다.
ALT
목과 스텁은 함께 쓸 수 있다.
CQS 원칙에 따르면 모든 메서드는 명령이거나 조회여야 하며, 이 둘을 혼용해서는 안된다.
명령
사이드 이펙트를 일으키고 어떤 값도 반환하지 않는 메서드(void 반환)다.
조회
사이드 이펙트가 없고 값을 반환한다.
식별할 수 있는 동작과 구현 세부 사항
모든 제품 코드는 2차원으로 분류할 수 있다.
공개 API(public) / 비공개 API (private)
식별할 수 있는 동작 / 구현 세부 사항
식별할 수 있는 동작이라면 아래 동작 중 하나라도 만족한다.
구현 세부 사항은 아래 두 가지 중 하나도 만족하지 않는다.
→ 클라이언트가 목표를 달성하는 데 도움이 되는 연산(operation)을 노출하라.
(연산은 계산을 수행하거나 부작용을 초래하거나 둘 다하는 메서드)
→ 클라이언트가 목표를 달성하는 데 도움이 되는 상태(state)을 노출하라.
잘 구현된 api 잘 구현되지 못한 api
잘 구현되지 못한 API는 public API를 통해 구현 세부 사항을 클라이언트에게 노출한다.
⇒ 불변성의 모순을 발생시킬 수도 있고, 리팩토링 내성도 약화시킬 수 있다.
구현 세부 사항 유출 : 연산의 예
159p 예저 5.5
User 클래스의 API가 잘 설계되지 않은 이유는 무엇일까?
속성과 메서드 모두 public으로 되어있다.
name 속성은 클라이언트의 목표를 달성하는데 도움이 되도록 세터를 노출한다.
normalizeName() 메서드도 작업이지만 클라이언트의 목표에 직결되지 않는다.
개선
⇒ 클래스 API를 잘 설계하려면 normalizedName() 메서드를 숨기고 속성 세터를 클라이언트 코드에 의존하지 않으면서 내부적으로 호출해야 한다.
구현 세부 사항 유출: 상태의 예
클라이언트에게 필요한 멤버는 render() 메서드 뿐
따라서 subRenderers 필드는 구현 세부 사항 유출이다.
클라이언트가 목표를 달성하는 데 직접적으로 도움이 되는 코드만 공개해야 하며, 다른 모든 것은 구현 세부 사항이므로 private로 해야한다.
Java
복사
public class MessageRenderer implements IRenderer{
public List<IRenderer> subRenderers;
public MessageRenderer() {
subRenderers = new ArrayList<>();
subRenderers.add(new HeaderRenderer());
subRenderers.add(new BodyRenderer());
subRenderers.add(new FooterRenderer());
}
@Override
public String render(final Message message) {
return String.join("",
subRenderers.stream()
.map(r -> r.render(message))
.collect(Collectors.toList())
);
}
}
목과 테스트 취약성 간의 관계
육각형 아키텍처 정의
도메인 계층 : 애플리케이션의 중심부이기 때문의 도표의 중앙에 위치
애플리케이션 서비스 계층 : 도메인 계층 위에 있으며 외부 환경과의 통신을 조정
각 계층의 API를 잘 설계하면 테스트도 프랙탈 구조를 갖기 시작한다.
달성하는 목표는 같지만 서로 다른 수준에서 동작을 검증한다.
애플리케이션 서비스 테스트 : 해당 서비스가 외부 클라이언트에게 매우 중요하고 큰 목표를 어떻게 이루는지 확인
도메인 클래스 테스트 : 큰 목표의 하위 목표를 검증
좋은 테스트라면, 어떤 테스트든 비즈니스 요구사항으로 거슬러 올라갈 수 있어야 한다. 각 테스트는 도메인 전문가에게 의미 있는 이야기를 전달해야하며, 그렇지 않으면 테스트가 구현 세부 사항과 결합되어 있으므로 불안정하다는 것을 강하게 암시한다.
이렇게 잘 설계된 API를 이용해서 코드 베이스를 검증하는 테스트는 식별할 수 있는 동작만 결합되어 있다. 따라서 비즈니스 요구사항만 잘 검증할 수 있고, 리팩토링 내성도 강하게 가질 수 있게 된다.
시스템 내부 통신과 시스템 간 통신
시스템 내부 통신 : 구현 세부 사항. 검증 대상이 아님.
시스템 간 통신 : 식별할 수 있는 동작. 검증 대상이 될 수 있음.
연산을 수행하기위한 도메인 클래스간 협력은 식별할 수 있는 동작이 아니기에 시스템 내부 통신은 구현 세부 사항에 해당한다.
이러한 협력은 클라이언트 목표와 직접적인 관계가 없기에 테스트가 취약해진다.
단위 테스트의 고전파와 런던파 재고
런던파는
불변 의존성을 제외한 모든 의존성에 목 사용을 권장한다.
시스템 간 통신 / 시스템 내부 통신을 구분하지 않는다.
시스템 내부 통신에도 목을 사용하기 때문에 세부 구현 사항과 강하게 결합해서 리팩토링 내성이 없어진다.
고전파는
테스트 간에 공유하는 의존성만 교체하자고 하므로 이 문제에 대해서 훨씬 유리하다.
고전파 역시 시스템 간 통신에 대한 처리에 이상적이지는 않다.
모든 프로세스 외부 의존성을 목으로 해야하는 것은 아니다.
고전파에서는 공유 의존성을 피할 것을 권고한다.
일반적인 접근법은 이러한 의존성을 테스트 대역, 즉 목과 스텁으로 교체하는 것
모든 외부에 있는 공유 의존성을 Mock으로 바꾸지 않아도 된다.
ex) 애플리케이션에서만 사용되는 데이터베이스
표
'책 > 단위 테스트' 카테고리의 다른 글
[단위 테스트] 7장 가치 있는 단위 테스트를 위한 리팩터링 (0) | 2025.03.18 |
---|---|
[단위 테스트] 6장 단위 테스트 스타일 (0) | 2025.03.18 |
[단위 테스트] 3장 단위 테스트 구조 (0) | 2025.03.18 |
[단위 테스트] 1장 단위 테스트의 목표 (0) | 2025.03.18 |