2. 테스트 더블
“테스트 더블”은 무엇인가?
테스트하려는 코드를 주변에서 분리하게 도와 주는
놈!
또는
테스트 작성 시 테스트 대상 코드와 상호작용하는
객체
3. 테스트 대상 코드와 협력 객체를 분리
출처 : Effective Unit Testing
4. 테스트 더블의 위력!
● 테스트 대상 코드를 격리한다.
● 테스트 속도를 개선한다.
● 예측 불가능한 실행 요소를 제거한다.
● 특수한 상황을 시뮬레이션한다.
● 감춰진 정보를 얻어낸다.
5. 예) 테스트 대상코드(Car)와 협력 객체(Engine과 Rout)
public class Car {
private Engine engine;
public Car(Engine engine) { this.engine = engine;}
public void start() { engine.start(); }
public void drive(Route route) {
for(Directions directions : route.directions()){
directions.follow();
}
}
public void stop(){ engine.stop(); }
}
6. 테스트 대상 코드를 격리한다.
● Car는 Engine과 Route를 직접 사용하지만, Directions는 Route를 통해 간접적으로
만 사용
● 즉, 협력 객체로 부터 Car를 격리하는 일은 Engine과 Route만 테스트 더블로 교체.
출처 : Effective Unit Testing
출처 : Effective Unit Testing
7. 테스트 속도를 개선한다.
● 만약 Car가 이동할 최단경로를 구할 때 Route가 가중 그래프 검색 알
고리즘을 이용한다면 ?
○ 알고리즘을 계산하느라, 속도가 느려짐
● 테스트 더블을 이용해서 사전에 계산해둔 경로를 반환 한다면?
○ 테스트는 눈부시게 빨리짐!
8. 예측 불가능한 실행 요소를 제거한다.
public class Route {
private Clock clock = new Clock();
private ShortestPath algorithm = new …();
public Collection<Directions> directions() {
if(clock.isRushHour()) {
return algorithm.avoidBusyIntersections();
}
return algorithm.calculateRouteBetween(...);
}
}
혼잡한 시간에는 경로 계산
결과가 달라진다!
● 비결정적인 요인을 다룰땐, 테스트 더블!
○ 항상 똑같은 시간을 알려주는 테스트 더블로 변경!
9. 특수한 상황을 시뮬레이션한다.
● Route가 Directions를 구할 때 구글 맵스를 이용한다고 가정 해보자.
○ 목적지까지 경로를 요청하는 도중 인터넷이 끊긴 경우 Route는
잘 대처하는지 어떡해 확인 할 수 있을까?
연결 요청을 처리하는 부분을 테스트 더블로 대체해서 예외 발생시키자!
10. 감춰진 정보를 얻어낸다.
● 시나리오
○ 누군가 Car의 시동을 걸면 Car는 Engine을 가동한다.
○ 이 동작이 실제로 일어났는지 어떻게 확인 할 수 있을
까?
● 역시 테스트 더블이 해결책!
11. 감춰진 정보를 얻어낸다.
public class CarTest {
@Test public void engineIsStartedWhenCarStarts() {
TestEngine engine = new TestEngine();
new Car(engine).start();
assertTrue(engine.isRunning());
}
}
public class TestEngine extends Engine {
private boolean isRunning;
public void start(){isRunning = true;}
public boolean isRunning(){return isRunning;}
}
13. 테스트 스텁 - 유난히 짧다.
● 스텁의 사전적 정의
○ 스텁 (명사) 끝이 잘렸거나 유난히 짧은 것
● 테스트 스텁의 목적은 원래의 구현을 최대한 단순한 것으로 대체 하는 것
스텁의 전형적인 모습! ( 아.무.것.도.하.지.않.아.요 )
public class LoggerStub implements Logger {
public void log(LogLevel level, String message) { }
}
14. 테스트 스텁 - 유난히 짧다.
public class LoggerStub implements Logger {
public void log(LogLevel level, String message) { // 여전히 아무 일도 하지 않는다. }
public LogLevel getLogLevel() {
return LogLevel.WARN; // 하드코딩된 값을 반환한다.
}
}
LogLevel을 반환하는 메서드 정의
스텁을 사용하는 세 가지 이유
1. 테스트는 대상 코드가 로깅하는 내용에는 전혀 관심 없다.
2. 가동 중인 로그 서버가 없으니 로깅은 어차피 실패했을 거다.
3. 테스트 스위트가 콘솔로 대량의 정보를 쏟아내는 건 바라지 않는다. (파일에 쓰는 건 별로 상관없다.)
15. 가짜 객체 - 뒤끝 없이 처리한다.
● 언제 사용?
○ 최소한의 행동을 취해주거나 입력값에 따라 다르게 행동 할때 사용!
● 왜 사용?
○ 진짜 객체를 사용할때 생기는 부수효과나 연쇄동작이 일어나지 않게 경
량화 및 최적화 한 것
16. 가짜 객체 - 뒤끝 없이 처리한다.
public class FakeUserRepository implements UserRepository {
private Collection<User> users = …. // 인메모리 데이터베이스
public void save(User user){
if( findById(user.getId()==null ) { users.add(user); }
}
public User findById(long id) {
for( User user : users ) {
if( user.getId() == id ) return user;
}
return null;
}
}
public interface UserRepository {
void save(User user);
User findById(long id);
}
17. 테스트 스파이 - 기밀을 훔친다.
● 언제 사용?
○ 입력 인자로 사용되는 객체가 테스트에 필요한 정보를 알려주는 API를 제
공하지 않을 때 유용
○ 구현 어디에도 전달된 메시지가 잘 기록되었는지 알려주는 메서드가 보이
지 않을 시
주의 : 테스트 스파이를 사용해야 하는 상황이 발생하면 설계가 제대로 되었는지 한번쯤 의심을 ;ㅁ;
18. 테스트 스파이 - 기밀을 훔친다.
코드 - 테스트에 필요한 정보를 제공하지 않아 테스트 스파이가 필요한 상황
public class DLog{
private final DLogTarget[] targets;
public DLog(DLogTarget… targets) {
this.targets = targets;
}
public void write(Level level, String message) {
for (DlogTarget each : targets) {
each.write(level, message);
}
}
}
public interface DLogTarget {
void write(Level level, String message);
}
19. 테스트 스파이 - 기밀을 훔친다.
public class DLogTest{
@Test public void writeEachMessage…() {
SpyTarget spy = new SpyTarget();
DLog log = new DLog(spy);
log.write(Level.INFO, “message”);
assertTrue(spy.received(Level.INFO, “message”);
}
private class SpyTarget implements DLogTarget {
private List<String> log = ….;
public void write(Level level, String message) {
log.add(concatenated(level, message));
}
boolean received(Level level, String message) {
return log.contains(concatenated(level, message));
}
private String concatenated(Level level, String message){
return level.getName() + “: “ + message;
}
}
}
코드 - 간단히 구현해본 테스트 스파이
20. Mock 객체 - 예기치 않은 일을 막아준다.
● 특정 조건이 발생하면 미리 약속된 행동을 취하고, 약속된 행동이 취해지지 않으면 바
로 테스트 실패가 된다.
○ 예
■ UserRespository 으로 설명 하자면, findById()의 파라미터로 123을 주면
null, 124를 주면 가짜 user객체를 반환하는 식이다.
● 메서드를 파라미터에 따라 다르게 처리하게 한 스텁 수준 이지만, 이게
다가 아니다.
■ 하지만 123,124 외에 인자를 넘겼거나 다른 메서드를 호출하면 테스트를
실패하게 만들 수 있다.
● 이전 보다(스텁,가짜,스파이) 훨씬 정교한 테스트를 만들 수 있다.
○ 메서드가 호출되었는지? 몇번 호출되었는지 확인 할 수 있다.
21. Mock 객체 - 예기치 않은 일을 막아준다.
● Mock 객체 라이브러리
○ Mockito
○ JMock
○ EasyMock
22. Mock 객체 - 예기치 않은 일을 막아준다.
public class TestTranslator {
@Test public void userInternetForTranslation() {
//given
final Internet internet = mock(Internet.class);
given(internet.get(with(containsString(“langpair=en%7Cfi”)))
.willReturn(returnValue(“{“translatedText”:”kukka”}”));
//when
Translator t = new Translator(internet);
String translation = t.translate(“flower”, ENGLISH, FINNISH);
//then
assertEquals(“kukka”, translation);
}
}
23. 테스트 더블 활용 지침
● 용도에 맞는 도구를 꺼내 써라
● 준비하고, 시작하고, 단언하라
● 구현이 아니라 동작을 확인하라
● 자신의 도구를 선택해라
● 종속 객체를 주입하라
24. 용도에 맞는 더블을 선택해라.
● “가독성” - 테스트를 가장 읽기 쉽게 만들어주는 선택
● 그 외
○ 두 객체 간 상호작용의 결과로 특정메서드가 호출되었는지 확인하고 싶다면 Mock 객체
○ …...
○ 이도 저도 아니라면 동전을 던져보자, 앞면 Mock, 뒷면 스텁
26. 구현이 아니라 동작을 확인하라.
● 검증 목적과 관련 없는 지극히 사소한 변경마저도 테스트 실패 한다면..
○ 못질을 너무 많이 해서 구멍이 송송 뚫린 불쌍한 목판처럼 되어 버린다.
● 테스트는 오직 한 가지만 검사해야 하고 그 의도를 명확히 전달하도록 작성되어야 한
다.
● 핵심
○ “구현이 아니라 동작을 검증 하자!”
27. 종속 객체를 주입하라
● 진짜 객체를 테스트 더블 객체로 교체할 수 있어야 한다. 교체할 수
없다면 단위 테스트 작성도 못한다.
○ 종속 객체를 private 필드에 저장하거나 팩토리 메서드 등을 통
해 외부로부터 얻도록 해야 함
○ 보통 생성자 주입 방식을 많이 애용 함