Загружаем каталог…
Загружаем каталог…
대역의 필요성 테스트를 작성하다 보면 외부 요인이 필요한 시점이 있다. 다음은 외부 요인이 테스트에 관여하는 주요 예이다. 테스트 대상에서 파일 시스템을 사용 테스트 대상에서 DB로부터 데이터를 조회하거나 데이터를 추가 테스트 대상에서 외부의 HTTP 서버와 통신 테스트 대상이 이런 외부 요인에 의존하면 테스트를 작성하고 실행하기 어려워진다. 테스트 대상 코드에서 사용하는 외부 API 서버가 일시적으로 장애가 나면 테스트를 원활하게 수행할수 없다. 내부에서 사용하는 DB라도 상황에 맞게 데이터를 구성하는 것이 항상 가능한 것은 아니다. 자동이체 기능을 예로 들어보자. 이 기능은 외부 업체가 제공하는 API를 이용해서 카드번호가 유효한지 확인하고 그 결과에 따라 자동이체 정보를 등록한다. 이 기능을 테스트하려면 정상 카드번호, 도난 카드번호, 만료일이 지난 카드번호가 필요하기 때문에 외부 업체에서 상황별로 테스트할 수 있는 카드번호를 받아와야 한다. TDD는 "테스트 작성 — 통과시킬 만큼 구현 → 리팩토링"의 과정을 짧은 흐름으로 반복해야 하는데 외부 업체에서 상황별 카드번호를 제공하지 않으면 테스트를 진행할 수 없게 된다. 외부 요인은 테스트 작성을 어렵게 만들 뿐만 아니라 테스트 결과도 예측할 수 없게 만든다. 예를 들어 카드 정보 검사 대행업체에서 테스트할 때 사용하라고 제공한 카드번호의 유효 기간이 한 달 뒤일 수 있다. 이 카드번호를 사용해서 성공한 자동이체 정보 등록 기능 테스트는 한 달 뒤에 유효 기간 만료로 실패하게 된다. 테스트에서는 외부 요인으로 인해 테스트가 어려울 때 외부 요인을 대신하는 대역이 외부 요인을 대신해서 테스트에 참여한다. 영어로 된 테스트 관련 글을 읽으면 test double이란 표현이 자주 나오는데 여기서 double은 본 장에서 설명하는 대역에 해당한다. 즉, test double은 테스트에서 진짜 대신 사용할 대역을 의미한다. 대역을 이용한 테스트 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; } } public class AutoDebitRegister_Stub_Test { private AutoDebitRegister register; private StubCardNumberValidator stubValidator; private StubAutoDebitInfoRepository stubRepository; @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()); } @Test void theftCard() { stubValidator.setTheftNo("1234567890123456"); AutoDebitReq req = new AutoDebitReq("user1", "1234567890123456"); RegisterResult result = this.register.register(req); assertEquals(CardValidity.THEFT, result.getValidity()); } @Test void validCard() { AutoDebitReq req = new AutoDebitReq("user1", "1234123412341234"); RegisterResult result = this.register.register(req); assertEquals(VALID, result.getValidity()); } } public interface AutoDebitInfoRepository { void save(AutoDebitInfo info); AutoDebitInfo findOne(String userId); } public class MemoryAutoDebitInfoRepository implements AutoDebitInfoRepository { private Map<String, AutoDebitInfo> infos = new HashMap<>(); @Override public void save(AutoDebitInfo info) { infos.put(info.getUserId(), info); } @Override public AutoDebitInfo findOne(String userId) { return infos.get(userId); } } public class AutoDebitRegister_Fake_Test { private AutoDebitRegister register; private StubCardNumberValidator cardNumberValidator; private MemoryAutoDebitInfoRepository repository; @BeforeEach void setUp() { cardNumberValidator = new StubCardNumberValidator(); repository = new MemoryAutoDebitInfoRepository(); register = new AutoDebitRegister(cardNumberValidator, repository); } @Test void alreadyRegistered_InfoUpdated() { repository.save( new AutoDebitInfo("user1", "111222333444", LocalDateTime.now())); AutoDebitReq req = new AutoDebitReq("user1", "123456789012"); RegisterResult result = this.register.register(req); AutoDebitInfo saved = repository.findOne("user1"); assertEquals("123456789012", saved.getCardNumber()); } @Test void notYetRegistered_newInfoRegistered() { AutoDebitReq req = new AutoDebitReq("user1", "1234123412341234"); RegisterResult result = this.register.register(req); AutoDebitInfo saved = repository.findOne("user1"); assertEquals("1234123412341234", saved.getCardNumber()); } } 대역을 사용한 외부 상황 흉내와 결과 검증 앞서 대역을 사용한 테스트에서 주목할 점은 다음 두 가지 없이 AutoDebitRegister에 대한 테스트를 수행했다는 점이다. 외부 카드 정보 API 연동 자동이체 정보를 저장한 DB StubCardNumberValidator를 사용해서 유효하지 않은 카드번호에 대한 테스트를 수행했다. 외부 업체에서 제공하는 카드 정보 API 연동 없이 AutoDebitRegister가 유효하지 않은 카드번호에 대해 올바르게 동작하는지 확인할 수 있었다. 비슷하게 도난 카드번호에 대한 테스트도 대역인 StubCardNumberValidator을 이용해서 진행할 수 있었다. 또한, 메모리를 사용한 MemoryAutoDebitInfoRepository를 이용해서 데이터가 올바르게 바뀌고 저장되는지 확인했다. 실제 DB를 연동하지 않고 AutoDebitRegister가 데이터 저장소에 데이터를 올바르게 반영하는지 확인할 수 있었다. DB를 연동하지 않고 메모리를 사용했기에 테스트 속도도 매우 빠르다. 아래 공통점은 대역을 이용해서 외부의 상황을 흉내 낸다는 점이다. StubCardNumberValidator: 카드 정보 API를 대신해서 유효한 카드번호, 도난 카드번호와 같은 상황을 흉내 낸다 MemoryAutoDebitInfoRepository: 특정 사용자에 대한 자동이체 정보가 이미 등록되어 있거나 등록되어 있지 않은 상황을 흉내 낸다. 또한, 대역을 이용하면 외부에 대한 결과를 검증할 수 있다. AutoDebitRegister는 자동이체 정보를 AutoDebitlnfoRepository에 저장하는데 테스트 코드는 메모리를 이용한 대역을 사용해서 저장 결과를 확인하고 있다. 대역 종류 구현에 따라 아래와 같이 대역을 구분할 수 있다. 대역 종류 설명 스텁(Stub) 구현을 단순한 것으로 대체한다. 테스트에 맞게 단순히 원하는 동작을 수행한다. StubCardNumberValidator가 스텁 대역에 해당한다. 가짜(Fake) 제품에는 적합하지 않지만, 실제 동작하는 구현을 제공한다. DB 대신에 메모리를 이용해서 구현한 MemoryAutoDebitInfoRepository가 가짜 대역에 해당한다. 스파이(Spy) 호출된 내역을 기록한다. 기록한 내용은 테스트 결과를 검증할 때 사용한다. 스텁이기도 하다. 모의(Mock) 기대한 대로 상호작용하는지 행위를 검증한다. 기대한 대로 동작하지 않으면 익셉션을 발생할 수 있다. 모의 객체는 스텁이자 스파이도 된다. 회원 가입 기능을 이용해서 대역을 살펴보자. 각 타입은 다음 역할을 수행한다. UserRegister: 회원 가입에 대한 핵심 로직을 수행한다. WeakPasswordChecker: 암호가 약한지 검사한다. UserRepository: 회원 정보를 저장하고 조회하는 기능을 제공한다. EmailNotifier: 이메일 발송 기능을 제공한다 public class UserRegisterTest { private UserRegister userRegister; private StubWeakPasswordChecker stubPasswordChecker = new StubWeakPasswordChecker(); private MemoryUserRepository fakeRepository = new MemoryUserRepository(); private SpyEmailNotifier spyEmailNotifier = new SpyEmailNotifier(); @BeforeEach void setUp() { userRegister = new UserRegister(stubPasswordChecker, fakeRepository, spyEmailNotifier); } @DisplayName("약한 암호면 가입 실패") @Test void weakPassword() { stubPasswordChecker.setWeak(true); assertThrows(WeakPasswordException.class, () -> { userRegister.register("id", "pw", "email"); }); } @DisplayName("이미 같은 ID가 존재하면 가입 실패") @Test void dupIdExists() { // 이미 같은 ID 존재하는 상황 만들기 fakeRepository.save(new User("id", "pw1", "email@email.com")); assertThrows(DupIdException.class, () -> { userRegister.register("id", "pw2", "email"); }); } @DisplayName("같은 ID가 없으면 가입 성공함") @Test void noDupId_RegisterSuccess() { userRegister.register("id", "pw", "email"); User savedUser = fakeRepository.findById("id"); assertEquals("id", savedUser.getId()); assertEquals("email", savedUser.getEmail()); } @DisplayName("가입하면 메일을 전송함") @Test void whenRegisterThenSendMail() { userRegister.register("id", "pw", "email@email.com"); assertTrue(spyEmailNotifier.isCalled()); assertEquals( "email@email.com", spyEmailNotifier.getEmail()); } } public class WeakPasswordException extends RuntimeException { } public interface WeakPasswordChecker { boolean checkPasswordWeak(String pw); } public class StubWeakPasswordChecker implements WeakPasswordChecker { private boolean weak; public void setWeak(boolean weak) { this.weak = weak; } @Override public boolean checkPasswordWeak(String pw) { return weak; } } public class UserRegister { private WeakPasswordChecker passwordChecker; private UserRepository userRepository; private EmailNotifier emailNotifier; public UserRegister(WeakPasswordChecker passwordChecker, UserRepository userRepository, EmailNotifier emailNotifier) { this.passwordChecker = passwordChecker; this.userRepository = userRepository; this.emailNotifier = emailNotifier; } public void register(String id, String pw, String email) { if (passwordChecker.checkPasswordWeak(pw)) { throw new WeakPasswordException(); } User user = userRepository.findById(id); if (user != null) { throw new DupIdException(); } userRepository.save(new User(id, pw, email)); emailNotifier.sendRegisterEmail(email); } } public interface UserRepository { void save(User user); User findById(String id); } public class MemoryUserRepository implements UserRepository { private Map<String, User> users = new HashMap<>(); @Override public void save(User user) { users.put(user.getId(), user); } @Override public User findById(String id) { return users.get(id); } } public class User { private String id; private String password; private String email; public User(String id, String password, String email) { this.id = id; this.password = password; this.email = email; } public String getId() { return id; } public String getEmail() { return email; } } public class DupIdException extends RuntimeException { } 대역과 개발 속도 TDD 과정에서 대역을 사용하지 않고 실제 구현을 사용한다면 다음과 같은 일이 벌어지게 된다. 카드 정보 제공 업체에서 도난 카드번호를 받을 때까지 테스트를 기다린다. 카드 정보 제공 API가 비정상 응답을 주는 상황을 테스트하기 위해 업체의 변경 대응을 기다린다. 회원 가입 테스트를 한 뒤에 편지가 도착할 때까지 메일함을 확인한다. 약한 암호 검사 기능을 개발할 때까지 회원 가입 테스트를 대기한다. 네 경우 모두 대기 시간이 발생한다. 도난 카드에 대한 테스트를 진행하기 위해 업체로부터 도난 카드번호를 받아야 한다. 바로 카드번호를 받을 수 있으면 좋지만 1~2일 이상 소요되는 경우도 있다. 회원 가입할 때 메일이 발송되는지 확인하려면 실제 이메일 주소를 이용해서 테스트를 진행해야 한다. 또한, 메일이 도착할 때까지 메일함을 확인해야 한다. 이메일 특성상 테스트를 실행하고 몇 분 뒤에 메일이 도착하기도 한다. 약한 암호 검사 기능을 다른 개발자가 구현하고 있다면 그 개발자가 구현을 완료할 때까지 약한 암호에 대한 회원 가입 테스트를 진행할 수 없다. 대역을 사용하면 실제 구현이 없어도 다양한 상황에 대해 테스트할 수 있다. 외부의 카드 정보 제공 API와 연동할 수 없는 경우에도 유효한 카드번호나 도난 카드번호에 대한 테스트를 진행할 수 있다. DB가 없어도 동일 ID가 이미 존재하는 상황을 테스트할 수 있다. 또한, 대역을 사용하면 실제 구현이 없어도 실행 결과를 확인할 수 있다. DB가 없어도 회원 데이터가 올바르게 저장되는지 확인할 수 있고 메일 서버가 없어도 이메일 발송 요청을 하는지 확인할 수 있다. 즉, 대역은 의존하는 대상을 구현하지 않아도 테스트 대상을 완성할 수 있게 만들어주며 이는 대기 시간을 줄여주어 개발 속도를 올리는 데 도움이 된다. 모의 객체를 과하게 사용하지 않기 모의 객체는 스텁과 스파이를 지원하므로 대역으로 모의 객체를 많이 사용한다. 하지만 모의 객체를 과하게 사용하면 오히려 테스트 코드가 복잡해지는 경우도 발생한다. 회원 가입 성공 테스트를 모의 객체를 이용해서 작성해보자. public class UserRegisterMockOvercaseTest { private UserRegister userRegister; private WeakPasswordChecker mockPasswordChecker = Mockito.mock(WeakPasswordChecker.class); private UserRepository mockRepository = Mockito.mock(UserRepository.class); private EmailNotifier mockEmailNotifier = Mockito.mock(EmailNotifier.class); @BeforeEach void setUp() { userRegister = new UserRegister(mockPasswordChecker, mockRepository, mockEmailNotifier); } @DisplayName("이미 같은 ID가 존재하면 가입 실패") @Test
То, что RADAR обнаружил и классифицировал для этой возможности. Это опубликованный источником текст, а не подтверждение, что предложение ещё действует.
[테스트 주도 개발 시작하기] 7장. 대역의 필요성 테스트를 작성하다 보면 외부 요인이 필요한 시점이 있다. 다음은 외부 요인이 테스트에 관여하는 주요 예이다. 테스트 대상에서 파일 시스템을 사용 테스트 대상에서 DB로부터 데이터를 조회하거나 데이터를 추가 테스트 대상에서 외부의 HTTP 서버와 통신 테스트 대상이 이런 외부 요인에 의존하면 테스트를 작성하고 실행하기 어려워진다. 테스트 대상 코드에서 사용하는 외부 API 서버가 일시적으로 장애가 나면 테스트를 원활하게 수행할수 없다. 내부에서 사용하는 DB라도 상황에 맞게 데이터를 구성하는 것이 항상 가능한 것은 아니다. 자동이체 기능을 예로 들어보자. 이 기능은 외부 업체가 제공하는 API를 이용해서 카드번호가 유효한지 확인하고 그 결과에 따라 자동이체…
Открыть источник