단위 테스트

DoTheTest·2025년 7월 29일
0

테스트 지식

목록 보기
12/24

단위 테스트는 현대 소프트웨어 개발에서 선택이 아닌 필수입니다. 버그를 가장 빠르고 저렴하게 잡을 수 있는 1차 방어선이자, 개발자가 자신의 코드를 자신감 있게 리팩토링할 수 있게 해주는 가장 강력한 안전망 역할을 합니다.
하지만 모든 단위 테스트가 좋은 테스트는 아닙니다. 잘못 작성된 단위 테스트는 오히려 유지보수를 어렵게 하고, 코드 변경을 방해하는 짐이 되기도 합니다. 이 글에서는 "좋은 단위 테스트란 무엇인가"에 대한 해답을 FIRST 원칙을 통해 알아보고, 단위 테스트의 독립성을 보장하는 핵심 기술인 테스트 대역(Test Double)에 대해 깊이 있게 다룹니다.


1. 좋은 단위 테스트의 조건: FIRST 원칙

좋은 단위 테스트가 갖춰야 할 5가지 속성을 앞글자를 따서 FIRST라고 부릅니다. 이 원칙들은 우리 코드가 신뢰할 수 있고, 유지보수하기 쉬운 테스트 스위트를 갖추는 데 필수적입니다.

F (Fast): 빨라야 한다

단위 테스트는 개발 과정에서 수시로, 심지어 코드를 한 줄 변경할 때마다 실행될 수 있어야 합니다. 수천 개의 테스트가 몇 초 안에 완료되어야 개발자에게 즉각적인 피드백을 줄 수 있으며, 이는 개발 흐름을 끊지 않고 생산성을 유지하는 데 매우 중요합니다. 테스트가 느리다면, 개발자들은 점차 테스트 실행을 기피하게 될 것입니다.

I (Independent/Isolated): 독립적이고 격리되어야 한다

각 테스트는 다른 테스트에 영향을 주거나 받아서는 안 됩니다. 어떤 순서로 실행되더라도 결과는 항상 동일해야 합니다. 하나의 테스트가 다른 테스트를 위한 데이터를 생성하거나 상태를 변경하는 '공유 상태'는 반드시 피해야 합니다. 이는 테스트 실패 시 원인을 명확하게 특정하고, 테스트를 병렬로 실행하여 속도를 높이는 데 필수적입니다.

R (Repeatable): 반복 가능해야 한다

테스트는 어떤 환경(내 로컬 PC, 동료의 컴퓨터, CI 서버 등)에서도 항상 동일한 결과를 내야 합니다. 네트워크, 데이터베이스, 파일 시스템 등 외부 환경에 의존하는 테스트는 더 이상 순수한 단위 테스트가 아닙니다. 이러한 외부 의존성은 테스트 결과를 예측 불가능하게 만드는 주된 원인입니다.

S (Self-Validating): 스스로 검증 가능해야 한다

테스트 결과는 로그 파일을 수동으로 확인하거나 다른 값과 비교하는 등의 추가적인 작업 없이, 그 자체로 성공(Green) 또는 실패(Red)로 명확하게 판명되어야 합니다. assert 구문을 통해 예상 결과와 실제 결과를 비교하는 코드가 반드시 포함되어야 하며, 테스트 결과는 "통과" 또는 "실패"라는 이진(boolean) 값으로 나와야 합니다.

T (Timely): 시기적절해야 한다

단위 테스트는 실제 코드를 작성하기 직전, 또는 실제 코드와 동시에 작성되어야 합니다. 이것이 바로 TDD(테스트 주도 개발)의 핵심입니다. 코드가 이미 복잡하게 완성된 후에 테스트를 작성하려고 하면, 테스트하기 어려운 구조가 되어 있거나 중요한 엣지 케이스를 놓치기 쉽습니다. 테스트를 먼저 생각하면, 자연스럽게 테스트하기 좋은(Testable) 코드를 설계하게 됩니다.


2. 단위 테스트의 독립성을 위한 기술: 테스트 대역 (Test Double)

FIRST 원칙 중 '독립성(I)'과 '반복 가능성(R)'을 지키기 위한 가장 중요한 기술이 바로 테스트 대역(Test Double)을 사용하는 것입니다. 테스트 대역은 테스트하려는 대상(SUT, System Under Test)이 의존하는 실제 객체를 대신하여, 테스트의 목적에 맞게 동작하는 가짜 객체입니다.
마치 영화 촬영에서 위험한 액션 장면을 스턴트 대역(Stunt Double)이 대신하는 것과 같습니다.

테스트 대역의 종류와 역할

많은 개발자가 Mock과 Stub을 혼용하지만, 이들은 명확히 다른 목적을 가집니다. Martin Fowler는 테스트 대역을 다음과 같이 분류했습니다.

Dummy (더미):

가장 단순한 형태로, 단지 인스턴스화될 수만 있는 객체입니다. 호출은 되지만 아무런 동작도 하지 않으며, 주로 파라미터를 채우는 용도로만 사용됩니다. (예: new DummyObject())

Stub (스텁):

테스트 중에 만들어진 호출에 대해 미리 준비된 답변을 제공합니다. "상태 검증(State Verification)"에 주로 사용됩니다.
예시: OrderService를 테스트할 때, 외부 CouponRepository에 의존한다고 가정해 봅시다. couponRepository.findById("SUMMER2024")가 호출되면, 실제 DB를 조회하는 대신, 미리 준비된 new Coupon(1000) 객체를 반환하도록 설정하는 것이 Stub입니다. 우리는 CouponRepository의 동작이 아니라, 1000원 쿠폰을 받았을 때 OrderService가 총액을 올바르게 계산하는지를 테스트합니다.

Spy (스파이):

Stub의 역할을 하면서도, 호출된 내용에 대한 정보를 기록합니다. 즉, 어떤 메서드가 몇 번 호출되었는지, 어떤 인자로 호출되었는지를 나중에 검증할 수 있습니다.

Mock (목):

미리 정의된 기대(Expectation)에 따라 호출이 이루어졌는지 검증합니다. "행위 검증(Behavior Verification)"에 주로 사용됩니다.
예시: 주문이 완료되면 NotificationService의 sendEmail() 메서드가 반드시 '한 번' 호출되어야 한다는 것을 검증하고 싶을 때 사용합니다. 테스트가 끝난 후, verify(notificationService, times(1)).sendEmail(...)와 같이 호출 여부와 횟수를 검증합니다. 이메일이 실제로 발송되었는지(상태)가 아니라, 발송하려는 '행위'가 있었는지가 중요합니다.

Fake (페이크):

실제 구현을 단순화하여 대체한, 동작하는 구현체입니다. 예를 들어, 실제 데이터베이스 대신 인메모리(In-Memory) 데이터베이스를 사용하는 것이 Fake의 좋은 예입니다. 여러 테스트 케이스에서 복잡한 Stub을 설정하는 것보다 효율적일 수 있습니다.

0개의 댓글