테스트 대역 은 실제 구현 대신 사용 할 수 있는 객체나 함수를 말합니다. 테스트 대역은 실제 구현보다 훨씬 가벼워서 여러 프로세스나 기기를 연동시켜야 할 때 빠르고 안정적인 작은 테스트로 대응할 수 있게 도와줍니다.
1. 테스트 대역이 소프트웨어 개발에 미치는 영향
테스트 대역을 사용하면 절충이 필요한 몇 가지 문제들:
- 테스트 용이성
- 테스트 대역을 사용하려면 코드베이스가 테스트하기 쉽도록 설계되어 있어야 합니다.
- 적용 가능성
- 테스트 대역을 제대로 활용하면 엔지니어링 속도가 크게 개선되겠지만, 잘못 사용하면 오히려 깨지기 쉽고 복잡한 나쁜 테스트로 전락합니다. 실제로 테스트 대역을 활용하기에 적절하지 않은 경우가 많으니 되도록 실제 구현을 이용하는 것을 권합니다.
- 충실성
- 충실성은 테스트 대역이 실제 구현의 행위와 얼마나 유사하냐를 나타냅니다. 테스트 대역에서 충실성은 실제보다 단순해야 되며 단위 테스트들만으로 채우지 못하는 부분은 실제 구현을 이용하는 더 큰 범위의 테스트로 보완해줘야 합니다.
2. 테스트 대역 @구글
구글이 어렵게 깨우친 교훈 하나는 테스트 대역을 쉽게 만들어주는 모의 객체 프레임워크를 과용하면 위험하다는 것입니다. 모의 객체는 의존하는 다른 모듈들에 신경 쓰지 않고 원하는 코드 조각에 집중하는 테스트를 매우 쉽게 만들 수 있습니다. 하지만 모의 객체를 이용한 테스트를 양산하면 실제 버그는 잘 찾아내지 못하고 끊임없이 코드를 보수했어야 했습니다. 오늘날에는 많은 엔지니어가 모의 객체 프레임워크를 피하고 실제에 더 가까운 테스트를 작성합니다.
3. 기본개념
효과적인 테스트 대역 사용법을 이야기하기 앞서 알아둬야 할 개념들입니다.
3.1 테스트 대역 예
class PaymentProcessor {
private CreditCardService creditCardService;
boolean makePayment(CreditCard creditCard, Money amount) {
if (creditCard.isExpired()) {
return false;
}
boolean success = creditCardService.chargeCreditCard(creditCard, amount);
return success;
}
}
테스트에서 실제 신용카드 서비스를 이용하는 건 어불성설이지만 테스트 대역에게 실제 시스템의 행위를 ‘흉내’ 내도록 할 수 있습니다. [코드 13-2]는 아주 간단한 테스트 대역의 예입니다.
class TestDoubleCreditCardService implements CreditCardService {
@Override public boolean chargeCreditCard(CreditCard creditCard, Money amount) {
return true;
}
}
비록 그리 유용해 보이지는 않는 대역이지만 이를 활용하면 makePayment ( ) 메서드의 로직 일부를 검증해 볼 수 있습니다. 예컨대 [코드 13-3]처럼 이 메서드가 만료된 신용카드를 올바 르게 처리해 주는지 검사할 수 있습니다. 이때의 코드 로직은 실제 신용카드 서비스에 접근하지 않기 때문이죠
@Test public void cardIsExpired_returnFalse() {
boolean success = paymentProcessor.makePayment(EXPIRED_CARD, AMOUNT);
assertThat(success).isFalse();
}
3.2 이어주기
이어 주기란 제품 코드 차원에서 테스트 대역을 활용할 수 있는 길을 터줘서 테스트하기 쉽게끔 만들어주는 걸 뜻합니다. 대표적인 이어 주기 기술로는 의존성 주입이 있습니다. 의존성 주입을 활용하는 클래스는 필요한 클래스를 내부에서 직접 생성하지 않고 외부에서 건네받습니다.
3.3 모의 객체 프레임워크
모의 객체 프레임 워크는 테스트 대역을 쉽게 만들어주는 소프트웨어 라이브러리입니다. 즉, 객체를 대역으로 대체할 수 있게 해 줍니다. 모의 객체는 구체적인 동작 방식을 테스트가 지정할 수 있는 테스트 대역을 말합니다. 모의객체 프레임워크를 과용하면 코드베이스를 유지보수하기 어렵게 된다는 부작용이 있습니다.
4. 테스트 대역 활용 기법
대표적인 테스트 대역 활용 기법은 세 가지입니다.
4.1 속이기(가짜 객체)
가짜 객체는 인메모리 데이터베이스처럼 제품 코드로는 적합하지 않지만 실제 구현과 비슷하게 동작하도록 가볍게 구현한 대역입니다.
// 가짜 객체는 빠르고 쉽게 만들 수 있습니다.
AuthorizationService fakeAuthorizationService = new FakeAuthorizationService();
AccessManager accessManager = new AccessManager(fakeAuthorizationService):
// 모르는 사용자의 ID로는 접근을 불허합니다.
assertFalse(accessManager.userHasAccess(USER_ID));
// 사용자 ID를 인증 서비스에 등록한 다음에는 접근을 허용합니다.
fakeAuthorizationService.addAuthorizedUser(new User(USER_ID));
assertThat(accessManager.userHasAccess(USER_ID)).isTrue();
테스트 대역이 필요할 때 가짜 객체가 멋진 해결사가 되어줄 수 있습니다. 하지만 적절한 가짜 객체가 아직 없다면 새로 작성해야 하는데, 실제 객체의 현재는 물론 ‘미래의 행위까지도 비슷하게 흉내 내야’ 하기 때문에 결코 쉽게 생각할 문제가 아닙니다.
4.2 뭉개기(스텁)
스텁은 원래 없던 행위를 부여하는 과정을 말합니다. 예컨대 대상 함수가 반환할 값을 지정한다고 하면, 이를 반환값을 뭉갠다(스텁 한다)라고 말합니다. 스텁은 보통 모의 객체 프레임워크를 이용해 수행합니다. 만약 모의 객체 프레임워크가 없었다면 원하는 값을 반환하도록 하드코딩한 클래스들을 직접 생성해야 됩니다.
// 모의 객체 프레임워크로 생성한 테스트 대역을 건넵니다.
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();
4.3 상호작용 테스트하기
상호작용 테스트란 대상 함수를 실제로 호출하지 않고도 그 함수가 ‘어떻게’ 호출되는지를 검증하는 기법입니다.
올바른 방식으로 호출되지 않으면 실패하는 테스트가 있을 수 있습니다. 예를 들어 함수가 전혀 호출되지 않거나, 너무 많이 호출되거나, 잘못된 인수와 함께 호출된다면 실패해야 하는 경우가 있습니다.
상호작용 테스트에도 주로 모의 객체 프레임워크를 활용합니다.
5. 실제구현
테스트 대역은 아주 값진 테스트 도구지만 구글은 가능하다면 대상 시스템이 의존하는 실제 구현을 사용합니다. 즉, 제품 코드가 사용하는 것과 똑같은 구현체를 사용합니다. 코드가 프로덕션 환경에서와 동일하게 동작해야 테스트 충실성이 높아지는데, 실제 구현을 이용하면 자연스럽게 그렇게 됩니다.
5.1 격리보다 현실성을 우선하자
의존하는 실제 구현을 이용하면 테스트 대상이 더 실제와 가까워집니다. 구글은 대상 시스템이 올바르게 동작한다는 확신을 높여주기 때문에 현실적인 테스트를 선호합니다.
5.2 실제 구현을 사용할지 결정하기
빠르고 결정적이고 의존성 구조가 간단하다면 실제 구현을 사용하는 게 좋습니다. 예컨대 값 객체라면 실제 구현을 사용해야 합니다. 다음과 같은 고려사항들을 염두에 두고 실제 구현 여부를 판단합니다.
실행시간
실제 구현의 수행 시간이 오래 걸릴 때는 테스트 대역이 유용합니다. 실행 시간의 느림을 판단하는 기준은 다양하기 때문에 애매한 수준이면 실제 구현을 사용하고 너무 느려졌다고 생각되는 때가 오면 테스트 대역을 투입하면 됩니다.
결정성
결정적인 테스트란 같은 버전의 시스템을 대상으로 실행하면 언제든 똑같은 결과를 내어주는 테스트를 말합니다.
반대로 대상 시스템은 그대로인데 결과가 달라지는 테스트를 비결정적이라고 합니다.
의존성 생성
극단적인 예로 객체를 다음과 같이 생성하는 테스트가 있다고 생각해 봅시다.
Foo foo - new Foo(new A(new B(new c()), new D()), new E()..., new z());
객체 각각을 생성하는 방법을 결정하는 데만도 시간이 꽤 걸릴 것입니다. 설상가상으로 이 객체들 중 단 하나의 생성자 시그니처만 바뀌어도 테스트까지 함께 수정해야 됩니다. 이쯤 되면 Mockito모의 객체 프레임워크를 이용한다면 단 한 줄로 대역을 생성할 수 있습니다.
@Mock Foo mockFoo;
그러나 코드가 팩토리 메서드나 자동 의존성 주입을 지원한다면 테스트에서도 똑같이 이용하는 게 좋습니다.
6. 속이기(가짜 객체)
실제 구현을 이용할 수 없을 때는 가짜 객체가 최선일 경우가 많습니다. 가짜 객체는 실제 구현과 비슷하게 동작하기 때문에 다른 테스트 대역들보다 우선적으로 활용됩니다.
6.1 가짜 객체가 중요한 이유
- 실제 객체를 사용할 때의 단점을 제거한 채 테스트를 효과적으로 수행할 수 있게 해 줍니다.
- API 테스트 경험이 극적으로 좋아집니다. 이를 모든 종류의 API까지 확장한다면 소프트웨어 조직 전반의 엔지니어링 속도를 크게 끌어올려줄 것입니다.
6.2 가짜 객체를 작성해야 할 때
팀에서 가짜 객체를 만들지 판단하려면 유지보수까지 포함한 비용과 가짜 객체를 사용해서 얻는 생산성 향상 정도를 잘 저울질해야 합니다. 사용할 사람이 많지 않다면 굳이 만들 필요가 없고 많다면 가짜 객체를 만듦으로써 생산성이 높아지는 경험을 할 수 있을 겁니다.
유지보수할 가짜 객체 수를 줄이려면 테스트에서 진짜 객체를 사용하지 못하게 만드는 근본 원인을 찾고 해당 코드만 가짜 객체로 만듭니다. 예를 들어 테스트에서 데이터베이스를 사용할 수 없다면 데이터베이스 API를 호출하는 클래스 각각이 아니라 데이터베이스 API자체만 가짜 객체로 만듭니다.
6.3 가짜 객체의 충실성
충실성이란 가짜 객체가 실제 구현의 행위를 얼마나 비슷하게 흉내 내느냐를 말합니다. 가짜 객체를 100% 충실하게 만들기는 어렵습니다. 그럼에도 가짜 객체는 실제 구현 API 명세에 가능한 한 충실해야 합니다. 가짜 객체는 실제 구현에 완벽히 충실해야 하지만 해당 테스트의 관점에서만 그렇게 해주면 충분합니다.
6.4 가짜 객체도 테스트해야
초기에는 올바르게 작동하던 가짜 객체라도 세월이 흘러 실제 구현이 변경되면 실제 동작과 달라지게 되므로 가짜 객체에도 고유한 테스트가 있어야 합니다. 가짜 객체용 테스틀 작성하는 방법을 명세 테스트라 하는데 이는 실제 구현과 가짜 객체 둘 다를 대상으로 하는 공개 인터페이스 검증 테스트를 작성하는 것입니다.
6.5 가짜 객체를 이용할 수 없다면
- API 소유자에게 만들어달라고 부탁하기
- 직접 작성하기
- 실제 구현을 사용하거나 다른 테스트 대역 기법을 이용하기
7. 뭉개기
스텁을 이용한 뭉개기는 원래는 없는 행위를 테스트가 함수에 덧씌우는 방법입니다. 테스트에 실제 구현을 대체할 수 있는 쉽고 빠른 방법입니다.
public void getTransactionCount() {
transactionCounter = new TransactionCounter(mockCreditCardServer);
// 스텁을 이용해 트랜잭션 3개를 반환합니다.
when(mockCreditCardServer.getTransactions()).thenReturn(newList(TRANSACTION_1, TRANSACTION_2, TRANSACTION_3));
assertThat(transactionCounter.getTransactionCount()).isEqualTo(3);
}
7.1 스텁 과용의 위험성
스텁은 적용하기 쉬워서 많은 엔지니어들을 유혹하지만 과용하면 테스트를 유지보수할 일이 늘어나서 오히려 생산성을 갉아먹곤 합니다. 스텁을 과용하면 다음과 같은 부작용이 있습니다.
- 불명확해진다.
- 깨지기 쉬워진다.
- 테스트 효과가 감소합니다.
7.2 스텁이 적합한 경우
스텁은 실제 구현을 포괄적으로 대체하기보다는 특정함수가 특정 값을 반환하도록 하여 대상 시스템을 원하는 상태로 변경하려 할 때 제격입니다.
8. 상호작용 테스트하기
상호작용 테스트는 대상 함수의 구현을 ‘호 줄 하지 않으면서’ 그 함수가 어떻게 호출되는지를 검증하는 기법입니다. 모의 객체 프레임워크를 활용하면 상호작용 테스트를 어렵지 않게 수행할 수 있습니다.
8.1 상호작용 테스트보다 상태 테스트를 우선하자
상태 테스트란 대상 시스템을 호출하여 올바른 값을 반환하는지, 혹은 대상 시스템의 상태가 올바르게 변경되었는지를 검증하는 테스트를 말합니다. 상태 테스트에 집중해야 훗날 제품과 테스트를 확장할 때 훨씬 유리합니다.
상호작용 테스트의 문제
- 대상 시스템이 특정 함수가 호출되었는지만 알려줄 뿐, 올바르게 작동하는지를 말해주지 못합니다.
- 특정 함수가 호출되는지 검증하려면 대상 시스템이 그 함수를 호출할 것임을 테스트가 알아야 합니다. 이는 제품 코드의 구현 방식이 바뀌면 테스트가 깨질 수 있습니다.
8.2 상호작용 테스트가 적합한 경우
- 실제 구현이나 가짜 객체를 이용할 수 없어서 상태 테스트가 불가능한 경우
- 함수 호출 횟수나 호출 순서가 달라지면 기대와 다르게 동작하는 경우, 예를 들어 데이터베이스 캐시 기능을 검증하려 한다면 데이터베이스가 특정 횟수 이하로 호출되는지를 확인하면 됩니다.
상호작용 테스트는 상태 테스트를 완전히 대체하지 못합니다. 따라서 단위 테스트에서 상태 테스트를 수행할 수 없다면 상호작용 테스트를 추가하는 대신 더 큰 범위의 테스트 스위트에서 상태 테스트를 수행하여 보완하는 게 좋습니다.
8.3 상호작용 테스트 모범 사례
- 상태 변경 함수일 경우에만 상호작용 테스트를 우선 고려하자
- 너무 상세한 테스트는 피하자
9. 마치며
테스트 대역을 활용하면 대상 코드를 포괄적으로 검증하고 테스트 속도를 높여줘서 엔지니어링 속도에 좋은 영향을 줍니다. 하지만 잘못 사용하면 생산성을 크게 떨어뜨립니다. 실제 구현을 사용할지 테스트 대역을 쓸지는 엔지니어가 각각의 장단을 고려하고 절충하여 상황에 적합한 방식을 택해야 합니다.
10. 핵심정리
- 테스트 대역보다는 되도록 실제 구현을 사용해야 합니다.
- 테스트에서 실제 구현을 사용할 수 없을 때는 가짜 객체가 최선일 때가 많습니다.
- 스텁을 과용하면 테스트가 불명확해지고 깨지기 쉬워집니다.
- 상호작용 테스트는 되도록 피하는 게 좋습니다. 상호작용 테스트는 대상 시스템의 상세 구현 방식을 노출하기 때문에 테스트를 깨지기 쉽게 만듭니다.
발제문
- 구글은 테스트 대역보다는 되도록이면 실제 구현을 이용한 테스트를 권장하고 있습니다. 하지만 제가 몸담고 있는 프로젝트에서 실제 구현을 활용한 테스트는 이루어지지 않고 있고, 심지어 테스트 대역을 사용한 기본 단위 테스트조차 잘 수행되지 않고 있습니다. 테스트 커버리지 비율을 따지면 전체의 1% 정도라고 할 수 있겠네요.. 만약 여러분들도 저와 같은 상황이라면 이런 문제의 원인이 무엇일까요? 개인적인 역량, 프로젝트 규모, 코드 품질 등 다양한 요인들이 이 문제에 영향을 미치고 있을 수 있습니다. 각각의 요인을 토대로, 우리가 겪고 있는 이슈를 분석하고, 이로 인한 문제를 어떻게 개선할 수 있을지에 대해 토론해 봅시다.
- "테스트 대역을 이용하면 테스트 속도를 향상하고, 대상 코드의 포괄적인 검증이 가능하다는 이점이 있습니다. 하지만 이러한 이점을 최대한 활용하기 위해서는 테스트 대역의 올바른 사용 방법과 전략이 필요합니다. 실제로 테스트 대역을 활용한 경험이 있다면 그 경험을 공유해 보시면 어떨까요? 특히, 가짜 객체의 '충실성'을 보장하면서 테스트 대역을 활용했던 사례가 있다면 그것을 중심으로 이야기해 주세요. 그리고 이를 통해 어떤 이점을 얻었는지, 또 어떤 어려움을 겪었는지도 함께 공유해 주시면 좋을 것 같습니다.”
- 스텁은 원래 없던 행위를 부여하는 과정을 말합니다. 예컨대 대상 함수가 반환할 값을 지정한다고 하면, 이를 반환값을 뭉갠다(스텁 한다)라고 말합니다. 스텁을 과도하게 사용하면, 테스트 코드가 복잡해지고 이해하기 어려워질 수 있습니다. 그 이유는 다음과 같습니다:
테스트 코드의 가독성 저하: 스텁을 많이 사용하면, 테스트 코드는 실제 테스트하려는 로직보다 스텁 설정에 대한 코드가 더 많아질 수 있습니다. 이로 인해 테스트 코드의 목적이 무엇인지 파악하기 어렵고, 코드를 읽는 사람이 혼란스러워 할 수 있습니다.
테스트의 신뢰성 저하: 스텁을 사용하면 테스트 대상 코드의 실제 동작을 모방하지만, 이는 항상 완벽하게 이루어지지 않습니다. 따라서 스텁을 과도하게 사용하면, 테스트 결과가 실제 시스템의 동작을 정확히 반영하지 않을 수 있습니다.
테스트의 의존성 증가: 스텁은 테스트 대상 코드의 특정 부분에 대한 동작을 직접 제어합니다. 따라서 스텁을 과도하게 사용하면 테스트가 테스트 대상 코드의 내부 구현에 의존하게 됩니다. 이는 테스트 대상 코드의 내부 구현이 변경될 때마다 테스트 코드도 수정해야 하는 문제를 야기할 수 있습니다.
"스텁을 과도하게 사용하여 테스트가 복잡하고 이해하기 어려워진 사례가 있다면, 그 사례를 공유하고, 이를 피하기 위한 대안은 무엇이었는지 이야기해 봅시다.”
본서
'독서 > 2024' 카테고리의 다른 글
[구글 엔지니어는 이렇게 일한다: 구글러가 전하는 문화, 프로세스, 도구의 모든것] CHAPTER 15 폐기 - 요약 & 발제문 (0) | 2023.07.30 |
---|---|
[구글 엔지니어는 이렇게 일한다: 구글러가 전하는 문화, 프로세스, 도구의 모든것] CHAPTER 14 더 큰 테스트 - 요약 & 발제문 (0) | 2023.07.29 |
[도메인 주도 개발 시작하기: DDD 핵심 개념 정리부터 구현까지] Chapter 1 - 도메인 모델 시작하기 (0) | 2023.06.21 |
[설득의 법칙-사람의 마음을 끌어당기는 10가지 심리학] - PART3. 전략 (0) | 2023.06.18 |
[설득의 법칙-사람의 마음을 끌어당기는 10가지 심리학] - PART2. 감정 (1) | 2023.06.13 |