Home 13. 테스트 대역
Post
Cancel

13. 테스트 대역

예를 들어 외부 서버에 요청을 보내고 응답을 받아 데이터베이스에 저장하는 함수를 검증하는 경우 테스트 몇 개만 작성하면 충분할 것이다. 하지만 이런 테스트를 수백, 수천개 작성하게 된다면 전체를 한 번에 수행하는데만 몇 시간씩 걸리고 예기치 못한 네트워크 실패나 테스트들끼리 테이터를 덮어쓰는 등의 일이 발생하여 테스트 스위트가 불규칙적으로 실패하기 시작할 것이다.

이런 상황에서는 테스트 대역이 아주 유용하다. 테스트 대역은 실제 구현 대신 사용할 수 있는 객체나 함수를 말한다.

아마도 가장 직관적인 유형의 테스트 대역은 실제와 비슷하게 동작하되 더 간단하게 구현한 객체일 것이다. (예. 인메모리 데이터베이스[휘발성 메모리])

13.1 테스트 대역이 소프트웨어 개발에 미치는 영향

  • 테스트 용이성 - 코드베이스가 테스트하기 쉽도록 설계되어 있어야 한다. 그래야 테스트에서 실제 구현을 테스트 대역으로 교체할 수 있다.
  • 적용 가능성 - 테스트 대역을 제대로 활용하면 엔지니어링 속도가 크게 개선되겠지만 잘못 사용하면 오히려 깨지기 쉽고 복잡하고 효율도 나쁜 테스트로 전락한다.
  • 충실성 - 테스트 대역이 실제 구현의 행위와 얼마나 유사하냐를 말한다.

13.2 테스트 대역 @구글

구글은 여러 경험들이 쌓여 테스트 대역을 올바르게 사용하는 관례를 발전시켰다. 어렵게 깨우친 교휸 하나로 테스트 대역을 쉽게 만들어주는 모의 객체 프레임워크를 과용하면 위험하다는 것이다. 모의 객체 프레임워크를 처음 도입했을 때는 만능 요술램프처럼 보였으나 몇 해가 지나자 커다란 대가를 치르게 되었다. 테스트를 작성하기는 쉬웠지만 버그는 잘 찾아내지 못했고 끊임없이 보수해야 했다.

그래서 오늘날에는 많은 엔지니어가 모의 객체 프레임워크를 피하고 실제에 더 가까운 테스트를 작성한다.

13.3 기본 개념

13.3.1 테스트 대역 예

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//예) 신용카드 서비스
class PaymentProcessor {
  private CreditCardService creditCardService;
  ....
  boolean makePayment(CreditCard creditCard, Money amount) {
    if(creditCard.isExpired()) {return false;}
    boolean success = creditCardService.chargeCreditCard(creditCard, amount);
    return success;
  }
}

//기초적인 테스트 대역
class TestDoubleCreditCardService implements CreditCardService {
  @Override
  public boolean chargeCreditCard(CreditCard creditCard, Money amount) {
    return true;
  }
}

//테스트 대역 적용
@Test
public void cardIsExpired_returnFalse() {
  boolean success = paymentProcessor.makePayment(EXPIRED_CARD, AMOUNT);
  assertThat(success).isFalse();
}

13.3.2 이어주기

단위 테스트를 고려해 짜인 코드를 테스트하기 쉽다라고 말한다. 그리고 이어주기는 제품 코드 차원에서 테스트 대역을 활용할 수 있는 길을 터줘서 테스트하기 쉽게끔 만들어주는 걸 뜻한다. 대표적인 이어주기 기술로는 의존성 주입이 있다.

1
2
3
4
5
6
7
8
9
class PaymentProcessor {
  private CreditCardService creditCardService;

  //생성자에서 CreditCardService의 인스턴스를 직접 생성하지 않고 대신 인수로 건네 받는다.
  PaymentProcessor(CreditCardService creditCardService) {
    this.creditCardService = creditCardService;
  }
  ...
}
1
2
//CreditCardService 인스턴스를 생성할 책임은 생성자를 호출하는 측에 주어진다. CreditCardService구현을 넘길수도 있고 다음과 같이 테스트 대역을 넘길 수 있다.
PaymentProcessor paymentProcessor = new PaymentProcessor(new TestDoubleCreditCardService());

13.3.3 모의 객체 프레임워크

모의 객체 프레임워크는 테스트 대역을 쉽게 만들어주는 소프트웨어 라이브러리이다. 즉 객체를 대역으로 대체할 수 있게 해준다. 모의 객체는 구체적인 동작 방식을 테스트가 지정할 수 있는 테스트 대역을 말한다. 업계에서 많이 쓰이는 프로그래밍 언어라면 대부분 모의 객체 프레임워크가 존재한다. (예. java의 mokito)

13.4 테스트 대역 활용 기법

13.4.1 속이기(가짜 객체)

가짜 객체는 실제 구현과 비슷하게 동작하도록 가볍게 구현한 대역이다. 인메모리 데이터베이스가 좋은 예이다.

1
2
3
4
5
6
7
8
9
10
//가짜 객체는 빠르고 쉽게 만들 수 있다.
AuthorizationService fakeAuthorizationService = new FakeAuthorizationService();
AccessManager accessManager = new AccessManager(fakeAuthorizationService);

//모르는 사용자의 ID로는 접근을 불허한다.
asserFalse(accessManager.userHasAccess(USER_ID));

//사용자 ID를 인증 서비스에 등록한 다음에는 접근을 허용한다.
fakeAuthorizationService.addAuthorizedUser(new User(USER_ID));
assertThat(accessManager.userHasAccess(USER_ID)).isTrue();

13.4.2 뭉개기(스텁)

스텁은 원래는 없던 행위를 부여하는 과정을 말한다. 예) 대상 함수가 반환할 값을 지정한다고 하면, 이를 반환값을 뭉갠다(스텁한다)라고 말한다.

1
2
3
4
5
6
7
8
9
10
//모의 객체 프레임워크로 생성한 테스트 대역을 건낸다.
AccessManager accessManager = new AccessManager(mockAuthorizationService);

//USER_ID에 해당하는 사용자를 찾지 못하면(null을 반환하면) 접근을 불허한다.
when(mockAuthorizationService.lookupUser(USER_ID)).thenReturn(null);
assertThat(accessManager.userHasAccess(USER_ID)).isFalse();

//null이 아니면 접근을 허용한다.
when(mockAuthorizationService.lookupUser(USER_ID)).thenReturn(USER);
assertThat(accessManager.userHasAccess(USER_ID)).isTrue();

13.4.3 상호작용 테스트하기

상호작용 테스트란 대상 함수를 실제로 호출하지 않고도 그 함수가 어떻게 호출되는지를 검증하는 기법이다.

스텁과 비슷하게 상호작용 테스트에도 주로 모의 객체 프레임워크를 활용한다.

13.5 실제 구현

테스트 대역은 좋은 테스트 도구지만 구글은 가능하다면 시스템이 의존하는 실제 구현을 사용한다. 즉 제품 코드가 사용하는 것과 똑같은 구현체를 사용한다. 코드가 프로덕션 환경에서와 동일하게 동작해야 테스트 충실성이 높아지는데 실제 구현을 이용하면 자연스럽게 그렇게 된다.

13.5.1 격리보다 현실성을 우선하자

의존하는 실제 구현을 이용하면 테스트 대상이 더 실제와 가까워진다.

좋은 테스트라면 어떤 구현을 사용하든 상관없어야 한다. 좋은 테스트는 구현이 어떻게 구성되었느냐의 관점이 아니라 검사할 API를 중심으로 작성되어야 한다.

13.5.2 실제 구현을 사용할지 결정하기

빠르고 결정적이고 의존성 구조가 간단하다면 실제 구현을 사용하는 게 좋다. 예를 들어 값 객체라면 실제 구현을 사용해야 한다. 금액, 날짜, 주소, 리스트, 맵 같은 컬렉션 클래스가 대표적인 값 객체다.

다음과 같은 고려사항들을 염두해 두고 판단하자

  • 실행시간 - 실제 구현의 수행시간이 오래걸릴때는 테스트 대역이 유용할 수 있다. 느린 것의 기준은 엔지니어마다 다르기 때문에 너무 느려졌다고 생각되는 때가 오면 테스트 대역을 투입하면 된다. 테스트 병렬화도 실행시간을 줄이는 데 효과적이다.
  • 결정성 - 같은 버전의 시스템을 대상으로 실행하면 언제든 똑같은 결과를 내어주는 테스트를 결정적인 테스트라고 한다. 반대로 대상 시스템은 그대로인데 결과가 달라지는 테스트를 비결정적이라고 한다. 테스트에서 비결정성은 불규칙한 결과로 이어진다. 결과가 자주 튄다면 테스트 대역 투입을 고려할 때가 온 것이다.
  • 의존성 생성 - 실제 구현을 이용하려면 의존 대상들도 모두 생성해야 한다. 이에 반해 테스트 대역은 대체로 다른 객체를 별로 사용하지 않는다. 따라서 생성하기가 훨씬 쉽다.

13.6 속이기(가짜 객체)

실제 구현을 이용할 수 없을 때는 가짜 객체가 최선일 경우가 많다. 가짜 객체는 실제 구현과 비슷하게 동작하기 때문에 다른 테스트 대역들보다 우선적으로 활용된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//실제 구현도 같은 인터페이스를 이용한다.
public class FakeFileSystem implements FileSystem {
  private Map<String, String> files = new HashMap<>();

  @Override
  public void writeFile(String fileName, String contents){
    files.add(fileName, contents);
  }

  @Override
  public String readFile(String fileName) {
    String contents = files.get(fileName);

    if(contents == null) {throw new FileNotFoundException(fileName);}

    return contents;
  }
}

13.6.1 가짜 객체가 중요한 이유

가짜 객체는 테스트를 도와주는 강력한 도구이다. 빠른 것은 물론이고 실제 객체를 사용할 때의 단점을 제거한 채 테스트를 효과적으로 수행할 수 있게 해준다.

13.6.2 가짜 객체를 작성해야 할 때

가짜 객체는 실제 구현과 비슷하게 동작하기 때문에 노력도 더 들고 도메인 지식도 더 필요하다. 또 실제 객체의 행위가 변경될 때마다 발맞춰서 갱신해야 하므로 유지보수도 신경써야 한다.

가짜 객체를 만들지 판단하려면 유지보수까지 포함한 비용과 가짜 객체를 사용해서 얻는 생산성 향상 정도를 잘 저울질해야 한다. 사용할 사람이 많지 않다면 굳이 시간들일 이유가 없지만 사용자가 수백명이라면 생산성이 높아지는 경험을 할 수 있을 것이다.

13.6.3 가짜 객체의 충실성

가짜 객체를 활용하는 핵심 이유는 충실성이 있을 것이다. 충실성은 가짜 객체가 실제 구현의 행위를 얼마나 비슷하게 흉내 내느냐를 말한다. 100% 충실하여 만들기는 어렵지만 그럼에도 가짜 객체는 실제 구현의 API 명세에 가능한 한 충실해야 한다. API를 통해 어떤 데이터를 건네든 가짜 객체는 실제 구현과 동일한 결과를 돌려주고 상태 변화도 똑같이 시뮬레이션해야 한다.

13.6.4 가짜 객체도 테스트해야

실제 구현의 API 명세를 만족하는지 확인하려면 가짜 객체에도 고유한 테스트가 딸려 있어야 한다. 실제 구현이 변경되면 실제 동작과 달라지게 되므로 자체 테스트로 이런 사태를 막아줘야 한다.

13.6.5 가짜 객체를 이용할 수 없다면

사용할 수 있는 가짜 객체가 없다면 가장 먼저 API 소유자에게 하나 만들어달라고 하면 된다. 그런데 소유자가 가짜 객체를 만들 생각이 없거나 만들 수 없다면 직접 작성할 수도 있다. 먼저 해당 API를 감싸는 클래스를 하나 만들어서 모든 API 호출이 이 클래스를 거쳐 이루어지게 한다. 그런 다음 인터페이스는 똑같지만 실제 API를 이용하지는 않는 클래스를 한 벌 더 준비한다. 이 클래스가 바로 가짜 객체다.

13.7 뭉개기(스텁)

스텁을 이용한 뭉개기는 원래는 없는 행위를 테스트가 함수에 덧씌우는 방법이다.

13.7.1 스텁 과용의 위험성

스텁은 적용하기 쉬워서 실제 구현을 이용하기가 여의치 않을 때마다 엔지니어들을 유혹한다. 하지만 스텁을 과용하면 테스트를 유지보수할 일이 늘어나서 오히려 생산성을 갉아먹곤 한다.

  • 불명확해진다 - 스텁을 이용하려면 대상 함수에 행위를 덧씌우는 코드를 추가로 작성해야 한다. 이 추가로 코드는 읽는 이의 눈을 어지럽혀서 테스트의 의도를 파악하기 어렵게 한다.
  • 깨지기 쉬워진다 - 스텁을 이용하면 대상 시스템의 내부 구현 방식이 테스트에 드러난다. 좋은 테스트라면 사용자에게 영향을 주는 공개 API가 아닌 한, 내부가 어떻게 달라지든 영향받지 않아야 한다.
  • 테스트 효과가 감소한다 - 원래 행위를 뭉개버리면 해당 함수가 실제 구현과 똑같이 동작하는지 보장할 방법이 사라진다. 또 스텁을 이용하면 상태를 저장할 방법이 사라져서 대상 시스템의 특성 일부를 테스트하기 어려울 수 있다.
  • 스텁을 과용한 예 - 364~365p

13.7.2 스텁이 적합한 경우

스텁은 실제 구현을 포괄적으로 대체하기보다는 특정 함수가 특정 값을 반환하도록 하여 대상 시스템을 원하는 상태로 변경하려 할 때 제격이다.

목적이 분명하게 드러나게 하려면 스텁된 함수 하나하나가 단정문들과 직접적인 연관이 있어야 한다. 그래서 테스트들은 대체로 적은 수의 함수만 스텁으로 대체한다.

13.8 상호작용 테스트하기

상호작용 테스트는 대상 함수의 구현을 호출하지 않으면서 그 함수가 어떻게 호출되는지를 검증하는 기법이다.

13.8.1 상호작용 테스트보다 상태 테스트를 우선하자

상태 테스트란 대상 시스템을 호출하여 올바른 값을 반환하는지, 혹은 대상 시스템의 상태가 올바르게 변경되었는지를 검증하는 테스트를 말한다.

구글은 오랜 경험을 통해 상태 테스트에 집중해야 훗날 제품과 테스트를 확장할 때 훨씬 유리하다는 사실을 깨달았다. 깨지기 쉬운 테스트가 줄어들고 나중에 테스트를 변경하거나 유지보수하기가 쉬워진다.

상호작용 테스트의 문제점으로는 다음과 같이 있다.

  1. 시스템이 특정 함수가 호출되었는지만 알려줄 뿐 올바르게 작동하는지는 말해주지 못한다.
  2. 대상 시스템의 상세 구현 방식을 활용한다. 특정 함수가 호출되는지 검증하려면 대상 시스템이 그 함수를 호출할 것임을 테스트가 알아야 한다.

13.8.2 상호작용 테스트가 적합한 경우

  • 실제 구현이나 가짜 객체를 이용할 수 없어서 상태 테스트가 불가능한 경우
  • 함수 호출 횟수나 호출 순서가 달라지면 기대와 다르게 동작하는 경우, 예) 데이터베이스 호출 횟수를 줄여주는 캐시 기능을 검증

상호작용 테스트는 상태 테스트를 완전히 대체하지 못한다. 따라서 단위 테스트에서 상태 테스트를 수행할 수 없다면 상호작용 테스트를 추가하는 대신 더 큰 범위의 테스트 스위트에서 상태 테스트를 수행하여 보완하는 게 좋다.

13.8.3 상호작용 테스트 모범 사례

  • 상태 변경 함수일 경우에만 상호작용 테스트를 우선 고려하자 - 일반적으로 상호작용 테스트는 상태 변경 함수에 한해서만 수행해야 한다.
  • 너무 상세한 테스트틑 피하자 - 어떤 함수들이 어떤 인수들을 받아 호출되는지를 너무 세세하게 검증하지 않는게 좋다. 그래야 테스트가 더 명확하고 간결해진다.

13.9 마치며

테스트 대역을 활용하면 대상 코드를 포괄적으로 검증하고 테스트 속도를 높여줘서 엔지니어링 속도에 아주 중요하다는 걸 알게되었다. 하지만 잘못 사용하면 테스트를 불분명하고, 깨지기 쉽고, 덜 효과적으로 만들어서 오히려 생산성을 크게 떨어뜨리기도 한다.

테스트 대역은 테스트에서 사용하기 어려운 의존성 문제를 멋지게 우회하게 도와준다.

13.10 핵심 정리

  • 테스트 대역보다는 되도록 실제 구현을 사용해야 한다.
  • 테스트에서 실제 구현을 사용할 수 없을 때는 가짜 객체가 최선일 때가 많다.
  • 스텁을 과용하면 테스트가 불명확해지고 깨지기 쉬워진다.
  • 상호작용 테스트는 되도록 피하는 게 좋다.
This post is licensed under CC BY 4.0 by the author.

12. 단위 테스트

14. 더 큰 테스트