05 목과 테스트 취약성
Mock, Stub 구분
- Mock
- mock, spy
- 외부로 나가는 상호작용을 모방하고 검사하는데 도움이 된다.
- 상호작용 : SUT가 상태를 변경하기 위한 의존성을 호출하는것.
- 관련 의존성간의 상호작용을 모방하고 검사한다.
- Stub
- stub, dummy, fake
- 내부로 들어오는 상호작용을 모방하는데 도움이 된다.
- 상호작용 : SUT가 입력 데이터를 얻기 위한 의존성을 호추하는 것.
- dummy : null이나 하드코딩된 값. sut인터페이스 만족이 목적이므로, 최종결과에 영향을 주지 않는다.
- stub : 시나리오마다 다른 값을 반환하게끔 구성
- fake : stub과 같지만, fake는 아직 존재하지않는 의존성을 대체하고자 구현한다. (?)
- 관련 의존성을 모방만 하고, 검증하지 않는다.
-
stub과의 상호작용을 검증하는 것은 취약한 테스트를 야기하는 일반적인 안티패턴 ```java // mock public this_is_mock(){ var mock = new Mock(Some.class); var sut = new Sut(mock);
sut.doSomething();
then(mock).should(only()).innerMethod(); }
//stub public this_is_stub(){ var stub = new Mock(Some.class); when(stub.innerMethod()).thenReturn(10); var sut = new Sut(mock);
var result = sut.createSomething();
assertThat(result).isEqualTo(10) //then(mock).should(only()).innerMethod(); <- over specification, 깨지기 쉬운 테스트. } ```
도구로서의 mock, 테스트로서의 mock
- 도구로서의 mock = Mock framework
- 테스트로서의 mock = mocking된 클래스의 인스턴스
mock이면서 stub인것
- stub 처럼 준비된 응답을 설정하고, mock처럼 검증을 하는데, 각각 가정,검증하는 메서드는 다른 경우.
mock과 stub은 CQRS처럼 Command,Query 와 관련이 있다.
- mock = Command = void = 부작용 초래(내부 상태 변경)
- stub = Query = return result = 부작용 없음 = 멱등성 보장.
식별할 수 있는 동작과 구현 세부사항
- 공개 API가 식별할 수 있는 동작의 범위를 넘어서면 시스템은 구현 세부사항을 유출한다.
- 클라이언트가 목표를 달성하는데 도움이 되는 작업(method)와 상태(field)를 노출하라
- 일반적으로 단일 목표를 달성하고자 클래스에서 호출해야하는 메서드가 1개이상인 경우, 세부사항 유출가능성이 있다. ```java // 유출 String normalizedName = user.normalize(newName); user.name = normalizedName;
//숨김 user.name = newName; // setter 내에서 normalize 메서드 호출. ```
잘 설계된 API와 캡슐화
- 캡슐화는 궁극적으로 단위테스트와 동일한 목표를 달성한다.
- 구현 세부사항을 숨기면 클래스 내부를 가릴 수 있기때문에, 손상시킬 위험이 적다.
- 데이터와 연산을 결합하면 해당 연산이 클래스의 불변성을 위반하지 않도록 할 수 있다.
Mock 과 테스트 취약성 간의 관계
육각형 아키텍처 (hexagonal architecture)
- 도메인(buisness logic) + 애플리케이션 서비스(db조회및 도메인로직 호출 ,db저장) = 육각형 = 하나의 애플리케이션
- 도메인 계층과 애플리케이션 서비스 계층간의 관심사 분리
- 도메인계층은 해당 비즈니스 로직에 대해서만 책임을 져야하며, 나머지는 제외해야한다.
- 애플리케이션 서비스에는 어떤 비즈니스로직도 있어선 안된다.
- 애플리케이션 내부 통신
- 서비스계층에서 도메인 계층으로 단방향 의존성 흐름을 규정한다.
- 도메인계층은 서비스계층에 의존해선 안된다.
- 애플리케이션 간의 통신
- 외부 애플리케이션은 애플리케이션 서비스 계층에 있는 공통 인터페이스를 통해 해당 애플리케이션에 연결되며, 도메인에 직접 접근 불가.
- 캡슐화를 잘 하면 테스트도 fractal 구조를 갖는다.
시스템 내부 통신과 시스템간의 통신
- 내부통신 : 애플리케이션 내 클래스간의 통신
- 클라이언트의 목표와 직접적인 연관이 없고, 구현 세부사항에 해당.
- mock이 사용되면 리팩터링 내성이 낮아진다.
- 시스템간의 통신 : 다른 애플리케이션과의 통신
- 애플리케이션에 항상 있어야하는 인터페이스이며,(리팩터링 범주가 아님) 해당 시스템을 식별할수있는 동작이다.
- mock을 사용하면 애플리케이션간의 통신패턴을 확인할때 좋다.
런던파와 고전파의 재고
- 런던파를 따라 목을 무분별하게 사용하면 구현 세부사항에 결합되어 리팩터링 내성이 낮아진다.
- 고전파는 테스트간에 공유하는 의존성(시스템간의 통신)만 mocking하므로 리팩터링내성에 유리하다.
모든 프로세스 외부 의존성을 목으로 해야하는것은 아니다
의존성 유형
- 공유 의존성 : 테스트간에 공유하는 의존성
- 고전파에서는 피하라고 한다. 테스트 병렬처리할수없기때문.
- 일반적으로는 이걸 Mock, stub으로 교체한다.
- 프로세스 외부 의존성 : 프로그램의 실행 프로세스 외에 다른 프로세스를 점유하는 의존성
- 완전 통제권을 가진 프로세스 외부 의존성에 mock을 사용하면 깨지기 쉬운 테스트가 된다. (ex DB)
- 비공개 의존성 : 공유하지 않는 모든 의존성
Mocks aren’t stubs 나중에 읽어보기