[Effective unit Testing] Chap3. 테스트 더블 |
-
스텁(stub)과 더미(dummy)가 등장한 가장 큰 이유는 제품 코드가 온전히 준비되기 전까지 사용할 대용품이 필요했기 때문이다.
대용품이 있다면 주변 모듈을 다 갖추지 못했더라도 코드 일부만을 따로 컴파일하고 실행해볼 수 있었다.
-
오늘날에는 쓰임새가 훨씬 다양해졌다.
종속 모듈 없이 컴파일하고 실행하려는 애초의 용도는 그 비중이 많이 줄었다.
대신 테스트 추종 프로그래머에 의해 다양한 테스트 전용 장치가 만들어지면서 요즘은 테스트 대상 코드를 격리하고, 속도를 개선하고, 예측 불가능한 요소를 제어하고, 특수한 상황을 시뮬레이션하고, 감춰진 정보를 얻어내는 등의 용도로까지 쓰이고 있다.
목적에 따라 비슷한 듯하면서도 다른 객체를 사용하는데 그 전부를 통틀어 테스트 더블이라 한다.
3.1. 테스트 더블의 위력
-
코드는 덩어리다.
서로 참조하는 코드들이 그물처럼 얽혀있다.
각각의 조각은 약속된 동작을 수행한다.
어떤 동작은 원자적이라 클래스나 메서드 하나 안에서 모두 처리되는 반면, 어떤 동작은 다른 코드 조각과의 교류를 통해 완성된다.
-
종종 어떤 코드 조각이 원하는 동작을 올바로 수행하는지 검증하려 할 때, 주변 코드를 모두 교체하여 테스트 환경 전반을 통제할 수 있다면 가장 좋다. 이렇게 테스트 대상 코드와 협력 객체를 잘 분리하면 좋다.
-
테스트 전용 장치가 필요한 이유는..
테스트 대상 코드를 격리한다.
테스트 속도를 개선한다.
예측 불가능한 실행 요소를 제거한다.
특수한 상황을 시뮬레이션한다.
감춰진 정보를 얻어낸다.
3.1.1. 테스트 대상 코드를 격리한다.
-
자바와 같은 객체지향 언어 관점에서 보면 테스트 대상 코드를 격리한다는 것은 세상의 모든 것을 두 가지로 분류한다는 것이다.
테스트 대상 코드
테스트 대상 코드와 상호작용하는 코드
-
테스트 대상 코드를 격리하겠다는 것은 테스트하려는 코드를 그 외의 모든 코드에서 떼어 놓겠다는 의미다.
그 결과로 테스트는 초점이 분명해지고, 이해하기도 쉬워지고, 설정하기도 간편해진다.
그 외의 모든 코드에는 테스트 대상 코드가 호출하는 코드도 포함된다.
3.1.2. 테스트 속도를 개선한다.
-
테스트 더블로 협력 객체를 대체하면 원본을 그대로 사용할 때보다 빨라진다는 이점이 부수적으로 따라온다.
때로는 이것이 주목적이 되기도 한다.
-
성능이 느린 부분은 테스트 더블로 교체하고, 성능이 느린 부분을 띠로 검증하는 것이 추천된다.
3.1.3. 예측 불가능한 실행 요소를 제거한다.
-
테스트란 동작을 정의하고 명세와 일치하는지 확인하는 작업이다.
대상 코드가 완벽히 결정적이라서 불확정 요소가 전혀 없다면 꽤 간단하고 명확한 일일 것이다.
코드와 테스트 코드가 결정적이 되려면 몇 번을 테스트하건 항상 같은 결과가 나오도록 해야 한다.
-
비결정적 요소의 가장 전형적인 예는 시간과 관련된 동작이라 볼 수 있다.
예측할 수 없는 요인을 다뤄야 할 때에도 역시나 테스트 더블이 해결책이 될 수 있다.
주사위의 경우 숫자의 출력 순서가 정해진 테스트 더블을 만들어서 교체해주면 게임을 검증하기가 한결 수월해진다.
마찬가지로 항상 똑같은 시간을 알려주는 테스트 더블로 시스템 시계를 갈아 끼워주면 훨씬 쉽게 예측할 수 있다.
-
협력 객체를 통해 변수를 제거해서 테스트하려는 조건과 시나리오를 명확히 하는 근본 이유는 결과에 영향을 주는 모든 요소를 결정적으로 만들기 위해서다.
현실에서는 드물게 발생하는 특수한 상황도 테스트 더블로 시뮬레이션 할 수 있다.
3.1.4. 특수한 상황을 시뮬레이션한다.
-
실행 도중 테스트가 컴퓨터의 네트워크 인터페이스를 비활성시키는 경우를 생각해보자.
이런 식의 네트워크 오류를 만들어내려면 테스트 더블 외에는 달리 방법이 없다.
3.1.5. 감춰진 정보를 얻어낸다.
-
테스트 더블의 존재 이유 마지막은 테스트가 얻을 수 없었던 정보에 접근하기 위해서다.
특히 자바에서라면 정보 노출이 다른 객체의 private 속성을 읽고 쓴다는 뜻으로 해석될 여지가 있다.
그런 목적으로 써야 할 경우도 있겠지만, 그보다는 테스트 대상 코드와 협력 객체 사이의 상호작용을 확인하는 것으로 이해하길 바란다.
public class Car{ private Engine engine; public void start(){ engine.start(); } //... } 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; } }
3.2. 테스트 더블의 종류
3.2.1. 테스트 스텁은 유난히 짧다.
-
스텁의 사전적 정의는 다음과 같다.
끝이 잘렸거나 유난히 짧은 것.
테스트 스텁(줄여서 그냥 스텁)의 목적은 원래의 구현을 최대한 단순한 것으로 대체하는 것이다.
가장 간단한 예라면 기본값을 반환하는 한 줄짜리 메서드로 가득 찬 클래스가 있겠다.
이해를 돕기 위해 시스템 운영 정보를 로그 서버로 전송하는 코드를 가정해보자.
로그 서버에 접근할 수 있는 유일한 통로는 Logger interface 이고, 그 안에서 로그 생성 메서드가 하나 정의되어 있다.
public class LoggerStub implements Logger{ public void log(LogLevel level, String message){ } }
로그와 관련 없는 기능을 검사할 때는 로그가 있는지도차 알 필요 없으니, 제대로 만든 스텁이라 볼 수 있다.
-
public class LoggerStub implements Logger{ public void log(LogLevel level, String msg){ } public LogLevel getLogLevel(){ return LogLevel.WARN; } }
getLogLevel 에서 WARN 만을 return 하는 것이 문제가 없는가?
테스트 스텁을 사용하는 데는 다음의 세 가지 훌륭한 이유가 있기에 괜찮다.
1. 테스트는 대상 코드가 로깅하는 내용에는 전혀 관심 없다.
2. 가동 중인 로그 서버가 없으니 로깅은 어차피 실패했을 거다.
3. 테스트 스위트가 콘솔로 대량의 정보를 쏟아내는 건 바라지 않는다. (파일에 쓰는 건 별로 상관없다.)
-
때론 하드코딩된 값만 반환하거나 텅 빈 void 메서드만으로 부족할 때가 있다.
가끔은 테스트 더블이 최소한의 행동은 취해주거나 입력값에 따라 다르게 행동했으면 할 때가 있다.
이런 상황이 오면 가짜 객체를 떠올리자.
3.2.2. 가짜 객체는 뒤끝 없이 처리한다.
-
테스트 스텁에 비하면 가짜 객체는 정성이 꽤 들어간 테스트 더블이다.
반환값을 하드코딩하는 테스트 스텁의 특성상 테스트 각자의 시나리오에 맞는 스텁을 따로 구현해야 한다.
그에 반해 가짜 객체는 진짜 객체의 행동을 흉내 내지만, 진짜 객체를 사용할 때 생기는 부수 효과나 연쇄 동작이 일어나지 않도록 경량화하고 최적화한 것이라 볼 수 있다.
-
가짜 객체의 쓰임새를 가장 잘 보여주는 예는 영속성이다.
public interface UserRepository{ void save(User user); User findById(long id); User findByUsername(String username); }
테스트 더블을 사용하지 않으면 이 저장소를 이용하는 코드를 검사하려는 모든 테스트는 어딘가에 있을 진짜 DB 를 찾으려 할 것이다.
public class FakeUserRepository implements UserRepository{ private Collection<User> users = new ArrayList<User>(); public void save(User user){ if (findById(user.getId()) == null){ user.add(user); } } public User findById(long id){ for(User user : users){ if (user.getId() == id) return user; } return null; } public User findByUsername(String username){ for (User user : users){ if (user.getUsername().equals(username)){ return user; } } return null; } }
이렇게 대용품을 만들어 사용하면 진짜 오리처럼 꽥꽥대고 뒤뚱거리지만, 진짜보다 훨씬 빠르게 걷도록 만들 수 있다.
-
진짜 객체가 너무 굼뜨거나 테스트 시점에 아직 사용할 수 없다면 테스트 스텁과 가짜 객체가 막혔던 숨통을 뚫어줄 것이다.
그러나 이 두 기본적인 테스트 더블만으로는 해결할 수 없는 상황도 있다.
예를 들어 사방이 벽으로 꽉 막힌 방안에 갇혀있는 코드가 설계한대로 동작하고 있는지 궁금하다면?
이럴 때에는 테스트 스파이와 접선해보는 것이 좋다.
3.2.3. 테스트 스파이는 기밀을 훔친다.
-
스파이는 여느 테스트 더블과 똑같은 방식으로 진짜 객체를 대체한다.
그리곤 전달된 메시지를 추적하는 임무는 테스트 스파이에게 맡겨두고, 일을 마친 후에 메시지가 잘 도착했는지 스파이로부터 브리핑받는다.
요약하면, 테스트 스파이는 목격한 일을 기록해두었다가 나중에 테스트가 확인할 수 있게끔 만들어진 테스트 더블이다.
가끔은 이 개념을 더 확장해서 완전한 Mock 객체로 발전시키기도 한다.
3.2.4. Mock 객체는 예기치 않은 일을 막아준다.
-
Mock 객체는 특수한 형태의 테스트 스파이다.
특정 조건이 발생하면 미리 약속된 행동을 취한다.
UserRepository interface 를 예로 Mock 객체를 설명하자면, findById() 의 파라미터를 123 을 주면 null 을 반환하고, 124 를 주면 앞서 저장한 User 를 반환하는 식이다.
여기까지는 스텁과 크게 다르지 않다.
하지만 Mock 을 사용하면 예기치 않은 일이 발생하자마자 바로 실패하는, 이전보다 훨씬 정교한 테스트도 가능하다.
예를 들어 123, 124 외의 인자를 넘기거나 아예 다른 메서드를 호출하면 테스트를 실패하게 만들 수 있다.
비슷한 식으로, findById() 를 허용 횟수보다 많이 호출하거나 예상했던 연계 동작이 일어나지 않았을 때 실패하게 하는 것도 가능하다.
-
성숙한 Mock 객체 라이브러리는 JMock , Mockito, EasyMock 등이 있다.
-
@Test public void usesInternetForTranslation() throws Exception{ Mockery context = new JUnit4Mockery(); final Internet internet = context.mock(Internet.class); context.checking(new Expectations(){{ one(internet).get(with(containsString(“langpair=en%7Cfi”))); will(returnValue(“{\”translatedText\”:\”kukka\”)”)); }}); Translator t = new Translator(internet); String translation = t.translate(“flower”, ENGLISH, FINNISH); assertEquals(“kukka”, translation); }
하지만 단언한 건 이것만이 아니다. Mock 객체는 기대한 동작이 정말로 일어나는지도 지켜본다.
Internet 객체는 get() 메서드가 딱 한번만 호출되었고, 그 때의 인자가 사전에 약속된 문자열을 포함하고 있었다는 것까지 함께 단언한 것이다.
3.3. 테스트 더블 활용 지침
3.3.1. 용도에 맞는 더블을 선택하라.
-
선택할 수 있는 테스트 더블은 많고 서로 조금씩 다르다.
엄격한 규칙이 있는건 아니지만, 보통은 몇 가지를 고려해 선택해주는 것이 좋다.
가장 명확한 원칙은 테스트를 가장 읽기 쉽게 만들어주는 선택을 하라는 것이다.
그래도 결정이 힘들다면 다음 방법을 시도해보는 것이 좋다.
두 객체 간 상호작용의 결과로 특정 메서드가 호출되었는지 확인하고 싶다면 Mock 객체를 써야 할 가능성이 높다.
Mock 객체를 사용하기로 했는데, 테스트 코드가 생각만큼 깔끔하게 정리되지 않는다면 더 단순한 테스트 스파이를 손수 작성해서도 똑같은 마술을 부릴 수 있는지 생각해보자.
협력 객체는 자리만 지키면 되고 협력 객체가 대상 객체에 넘겨줄 응답도 테스트에서 통제할 수 있다면 스텁이 정답이다.
필요한 서비스나 컴포넌트를 미처 준비하지 못해 스텁을 대용품으로 사용하고 있는데, 시나리오가 너무 복잡해서 벽에 부딪혔거나 테스트 코드가 관리하기 어려울 만큼 복잡해졌다면 가짜 객체를 구현하는 걸 고려해보자.
이도 저도 아니라면 동전을 던져보자. 앞면이 Mock 객체, 뒷면이 스텁이다. 어쩌다 똑바로 서버리면 가짜 객체를 써도 뭐라 하지 않겠다.
-
스텁은 질문하고 Mock 은 행동한다.
3.3.2 준비하고, 시작하고, 단언하라.
-
준비-시작-단언(AAA, arrange-act-assert)라는 규약은, 필요한 객체들을 준비하고, 실행하고, 결과를 단언하는 총 세 단계로 테스트를 구성한다.
-
GIVEN, WHEN, THEN
‘행위 주도 개발’ 진영에도 준비-시작-단언과 유사한 용어와 구조가 사용된다.
(Given) 주어진 상황에서, (When) 어떤 일이 발생했을 때, (Then) 특정 결과를 기대한다.
이 작품은 우리가 기대하는 행위(동작)를 더 직관적인 언어로 표현하기 위해 고뇌한 결과물이다.
준비-시작-단언이 외우기에는 더 쉬울지 몰라도 given-when-then 이 흐름을 보면 더 부드럽고 (구현이 아닌) 행위라는 관점에서 사고를 더 자연스럽게 표현할 수 있다.
-
GIVEN WHEN THEN 구조는 꽤 광범위하게 쓰이며 테스트가 산만해지지 않게 지탱해준다.
혹시 세 영역 중 하나가 비대하다고 느껴진다면, 너무 많은 것을 한꺼번에 검사하려는 테스트일 가능성이 높다.
더 작은 단위의 기능을 집중적으로 검사하는 테스트로 나눌 필요가 있다는 신호다.
3.3.3. 구현이 아니라 동작을 확인하라.
-
Mock 객체에 예상을 너무 상세하게 설정하는 프로그래머가 있는데, 이는 흔히 저지르는 실수 중 하나다.
테스트와 관련된 모든 것을 Mock 객체로 만들고, 객체 간의 사소한 메서드 호출 하나까지 깐깐하게 정의하는 걸 말한다.
-
테스트는 무언가 잘못 변경되면 즉시 실패하여 우리에게 알려주리라는 믿음을 주어야 한다.
검증 목적과 관련 없는 지극히 사소한 변경마저도 테스트를 실패하게 하면 안 된다.
-
테스트는 오직 한 가지만 검사해야 하고, 그 의도를 명확히 전달하도록 작성되어야 한다.
3.3.4. 자신의 도구를 선택하라.
-
어떤 도구를 선택해도 되지만, 중요한 것은 지금이나 앞으로나 읽기 쉽고 간결하고 관리하기 쉬워야 한다는 것이다.
-
레거시 코드를 구원할 때는 Mockito 를, 새로운 기능을 설계할 때는 JMock 을 사용한다.
JMock 과 Mockito 는 기본 가정이 서로 달라서 각자의 목적에 더 유리하도록 진화되었다.
기본적으로 JMock 의 테스트 더블(Mock 객체)은 대상 코드가 그 어떤 것도 자기 마음대로 호출하면 안 된다고 믿는다.
이 가정을 완화하려면 스텁을 사용해야 한다.
이와 반대로, Mockito 의 테스트 더블(Mock 객체)은 대상 코드가 언제건 아무것이나 호출해도 된다고 믿는다.
이 가정을 더 강화하려면 우리가 직접 메서드 호출 여부를 확인해아 한다.
3.3.5. 종속 객체를 주입하라.
-
진짜 객체를 다른 것과 교체할 방법이 없다면 테스트 더블도 무용지물이다.
종속 객체를 다른 객체로 교체해야만 쉽게 검사할 수 있다면 그 객체를 사용하는 곳과 생성하는 곳을 달리 하는 것이 좋다.
즉, 종속 객체를 private 필드에 저장하거나 팩토리 메서드 등을 통해 외부로부터 얻도록 해야 한다.
그러나 private 필드에 저장하는 방법은 테스트 시 public 이나 패키지 디폴트로 변경하는, 즉 캡슐화 원칙을 무시하고 접근제한자를 수정하거나 리플렉션 API 로 private 필드에 객체를 강제 할당하는 방법을 써야 한다.
그래서 “종속 객체 주입법” 을 통해 종속 객체를 외부에서 안으로 집어넣는 방법이 훨씬 좋다.
보통 "생성자 주입" 방식을 많이 애용한다.
3.4. 요약
-
테스트 대상 코드를 격리해야지만 모든 시나리오를 시뮬레이션하고 모든 동작을 검증할 수 있다.
-
종종 테스트 시간을 단축할 목적으로도 테스트 더블을 이용한다.
테스트 더블을 활용하면 예측 불가능한 요소를 예측할 수 있게 바꿀 수 있어 테스트 인생을 훨씬 안락하게 만들어준다.
-
테스트 더블을 통해서만 시뮬레이션 할 수 있는 특수한 상황이 있을 수도 있고, 테스트 전용 코드를 넣기 위해 제품 설계를 바꾸는 꺼림직한 일도 테스트 더블로 막을 수 있다.
-
테스트 더블은 테스트 스텁, 가짜 객체, 테스트 스파이, Mock 객체라는 네 가지 형태가 있다.
극한까지 단순화된 테스트 스텁은 불필요한 협력 객체를 처내는 데 가장 효과적이다.
가짜 객체는 진짜 객체가 사용하기 어렵거나 귀찮을 때 쓸 수 있는 번개처럼 빠른 대용품이다.
테스트 스파이는 본래는 접근할 수 없었던 내부 정보가 궁금할 때 사용하면 좋다.
Mock 객체는 동작을 동저으로 재설정하거나 기대한 상호작용이 정말로 일어났는지 확인할 수 있도록 특화된 테스트 스파이다.
-
테스트 더블 활용 지침은..
첫째로, 당면한 상황과 조건에 적합한 테스트 더블을 선택한다.
Mock 객체를 사용할 떄는 핵심과 거리가 먼 자잘한 구현은 확인하지 않는 것이 중요하다.
의도한 동작이 제대로 일어났는지 검사했다면 Mock 객체가 해야 할 일은 충분히 한 것이다.
-
종속 객체를 대상 코드 안에서 직접 생성하는 것보다, 외부에서 주입할 수 있도록 통로를 열어 두는 것이 검사를 훨씬 쉽게 하는 지름길이다.
'프로그래밍 놀이터 > 안드로이드, Java' 카테고리의 다른 글
[Effective Unit Testing] Chap5. 유지보수성 (0) | 2019.03.14 |
---|---|
[Effective Unit Testing] Chap4. 가독성 (0) | 2019.03.13 |
[Effective Unit Testing] Chap2. 좋은 테스트란? (0) | 2019.02.27 |
[Effective Unit Testing] Chap1. 좋은 테스트의 약속 (0) | 2019.02.26 |
[Gradle] compile(api) vs. implementation (1) | 2019.02.06 |
댓글