목차
대역 필요성
"자동이체 기능"을 테스트하기 위한 외부 서비스에서 제공되는 정보를 이용하여 구현 시 문제가 되는 상황들
- 해당 업체에서 상황별로 테스트 할 수 있는 카드번호 받아야함
- 외부서비스에서 실수로 유효한 카드번호 삭제
- 발급 받은 유요한 카드번호의 유효기간 지남
외부 서비스에서 제공하는 HTTP URL을 이용하여 제공되는 정보 구현이 어려움
외부요인이 테스트에 관여하는 주요 예시
- 테스트 대상에서 파일 시스템을 이용
- 테스트 대상에서 DB로 부터 데이터를 조회하거나 데이터를 추가
- 테스트 대상에서 외부 HTTP 서버와 통신
TDD 는 "테스트 작성 -> 통과시킬 만큼 구형 -> 리팩토링" 의 과정을 짧은 흐름으로 반복해야 한다.
하지만, 테스트 대상이 이런 외부 요인에 의존하면 테스트를 작성하고 실행하기 어려워지며 실행 결과를 예측할 수 없게 만든다.
→ 이럴 때 외부 요인을 대신하는 대역을 이용하여 테스트를 작성한다.
대역을 이용한 테스트
대역을 이용하여 "자동이체 기능" 을 테스트하는 코드 작성해보기
CardNumberValidation을 대신한 대역 클래스
public class StubCardNumberValidator extends CardNumberValidator {
private String invalidNo;
private String theftNo;
public void setInvalidNo(String invalidNo) {
this.invalidNo = invalidNo;
}
public void setTheftNo(String theftNo) {
this.theftNo = theftNo;
}
@Override
public CardValidity validate(String cardNumber) {
if (invalidNo != null && invalidNo.equals(cardNumber)) {
return CardValidity.INVALID;
}
if (theftNo != null && theftNo.equals(cardNumber)) {
return CardValidity.THEFT;
}
return CardValidity.VALID;
}
}
- StubCardNumberValidation 에서 실제 카드번호 검증 기능을 구현하지 않고, VALID, INVALID 를 리턴하는 단순한 구현으로 대체
테스트 코드에 적용
....
@BeforeEach
void setUp() {
stubValidator = new StubCardNumberValidator();
stubRepository = new StubAutoDebitInfoRepository();
register = new AutoDebitRegister(stubValidator, stubRepository);
}
@Test
void invalidCard() {
stubValidator.setInvalidNo("111122223333");
AutoDebitReq req = new AutoDebitReq("user1", "111122223333");
RegisterResult result = this.register.register(req);
assertEquals(INVALID, result.getValidity());
}
이와 같이 대역을 이용하면 필요한 외부 서비스 api 를 흉내내서 테스트에 적용할 수도 있다.
또한, DB 없이 DB 역할을 하는 클래스를 간단한게 구현하여 적용할 수도 있다.
대역의 종류
대역 종류 |
설명 |
스텁(Stub) |
구현을 단순한 것으로 대체한다. 테스트를 위해 필요한 값만 제공하면 된다. |
가짜(Fake) |
제품에는 적합하지 않지만, 실제 동작하는 구현을 제공한다. DB 대신에 메모리를 이용해서 구현하는 방식이 이에 해당한다. |
스파이(Spy) |
호출된 내역을 기록한다. 기록한 내용은 테스트 결과를 검증할 때 사용한다. |
모의(Mock) |
기대한 대로 상호작용하는지 검증한다. 기대한 대로 동작하지 않으면 익셉션을 발생할 수 있다. 모듸 객체는 스텁이자 스파이도 된다. |
❏ 약한 암호 확인 기능에 스텁 사용 ➡︎ 테스트를 위해 필요한 값 (암호가 약하다고 응답)만 제공
- 실제 동작을 구현할 필요 x → 단순하게 약한 암호인지만 알려주면 된다.(스텁)
- 스텁으로 하여금 암호 확인 요청이 오면 암호가 약하다고 응답하라고 설정
- 실제 테스트 하는 클래스에 스텁용 클래스를 주입하여 결과를 리턴
❏ 리포지토리를 가짜 구현으로 사용 ➡︎ 리포지토리의 기본 기능을 구현
- 동일한 ID를 가진 회원이 존재할 경우 익셉셥을 발생하는 테스트 → 가짜로 회원을 추가
- User 클래스를 생성 - 엔티티 구현
- 가짜 DB 로 MemoryUserRepository 클래스 구현
- UserRepository 를 구현해서 가짜 DB에 메서드 추가
Note : 상수를 이용하여 테스트를 통과시킨 다음에 구현을 일반화할 방법이 떠오르지 않으면
예를 추가하면서 점진적으로 구현을 완성해 나가면 된다.
❏ 이메일 발송 여부를 확인하기 위해 스파이를 사용
회원 가입에 성공하면 회원 가입 안내 메일 발송 여부를 확인하는 방법
- 회원 가입 후 EmailNotifier "메일 발송 기능" 을 실행할 때 "가입한 이메일 주소" 를 사용했는지 확인
EmailNotifier 의 스파이 대역(class SpyEmailNotifier) 구현하여 이메일 발송여부를 확인
- 이메일 발송 기능을 호출 → SpyEmailNotifier 에서 호출 됨를 저장하여 True 로 반환
- 이메일 발송 기능을 호출 → SpyEmailNotifier 에서 가입시 사용된 이메일을 저장하여 해당 이메일을 반환
assertTrue(spyEmailNotifier.isCalled());
assertEquals("email@email.com", spyEmailNotifier.getEmail());
❏ 모의 객체로 스텁과 스파이 대체
- 주요기능 : 대역 객체(스텁, 스파이) 가 기대하는대로 상호작용했는지 확인하는 것
모의 객체를 위한 도구를 이용하면, 모의 객체가 기대하는대로 상호작용했는지 검증이 가능하다.
상황과 결과 확인을 위한 협업 대상(의존) 도출과 대역 사용
외부 API 를 직접 연동하여 테스트 코드에서 상황을 구현하거나 결과를 확인하기 어렵다.
이렇게 제어하기 힘든 외부상황이 존재하면 아래의 방법으로 의존을 도출하고 이를 대역으로 대신 할 수 있다.
- 제어하기 힘든 외부 상황을 별도 타입으로 분리
- 테스트 코드는 별도로 분리한 타입의 대역을 생성
- 생성한 대역을 테스트 대상의 생성자 등을 이용해서 전달
- 대역을 이용해서 상황 구성
"자동 이체 정보 등록 기능" 에서 외부 상황 분리
- 카드번호가 유효한지 검사 하는 기능을 별도 타입으로 분리하고 대역을 생성
- 별도 타입 : 카드번호 검사 기능 → 대역을 이용하여 구현 및 생성
회원 가입에 성공한 경우 이메일을 발송하는 기능
- 회원 가입 기능 실행 이후에 이메일 발송 여부를 확인할 수단 필요
- 이메일 발송 자체를 UserResiter 에서 구현하면 발송 여부를 확인하기 어려움
- 결과 확인과 관련된 기능을 별도 타입으로 분리 → 대역으로 대체
당장 구현하기 어렵거나 오래걸리는 로직도 분리하기에 좋은 후보다.
대역과 개발 속도
TDD 과정에서 대역을 사용하지 않고 실제 구현을 사용한다면 발생하는 상황들
- 카드 정보 제공 업체에서 도난 카드번호를 받을 때까지 테스트를 기다린다.
- 카드 정보 제공 API가 비정상 응답을 주는 상황을 테스트하기 위해 업체의 변경 대응을 기다린다.
- 회원 가입 테스트를 한 뒤에 편지가 도착할 때까지 메일함을 확인한다.
- 약한 암호 검사 기능을 개발할 때까지 회원 가입 테스트를 대기한다.
모두 대기 시간이 발생 ➔ 대역을 사용하면 실제 구현이 없어도 실행 결과를 확인 가능
대역은 의존하는 대상을 구현하지 않아도 테스트 대상을 완성할 수 있게 만들어주며
이는 대기시간을 줄여주어 개발 속도를 올리는데 도움이 된다.
모의 객체를 과하게 사용하지 않기
모의 객체 사용의 편리성
- 대역 클래스를 직접 만들 필요 없이 테스트 가능.
문제점
- 결과 검증 코드가 길고 복잡해짐.
- 여러 모의 객체 사용 시 코드 복잡도 증가.
- 메서드 호출 검증 위주로 상호 작용이 조금만 바뀌어도 테스트가 깨질 위험.
주의사항
- 모의 객체의 메서드 호출을 결과 검증의 주 수단으로 사용하지 말 것.
대안
- DAO나 리포지토리 등 저장소에 대한 대역으로 메모리 기반의 가짜 구현 사용 권장.
- 테스트 코드 관리가 용이해짐.
실용적 접근법
- 모의 객체는 필요한 곳에 최소한으로 사용
- 단위 테스트에서 모의 객체를 과도하게 사용하는 것보다, 통합 테스트를 통해 실제 상호작용을 검증하는 것이 좋음