Code/Test

[단위 테스트] 4-1. 좋은 단위 테스트의 4대 요소: 4대 요소

noahkim_ 2025. 1. 19. 03:29

블라디미르 코리코프 님의 "단위 테스트" 책을 정리한 포스팅입니다.


1. 좋은 단위 테스트의 4대 요소

요소 설명 문제 상황 / 원인 해결 방안
회귀 방지 기존 기능이 수정으로 인해 망가지지 않도록 방지 코드 변경 후 기능이 동작하지 않음
테스트를 통한 기능 보호
책임 있는 코드 작성
리팩터링 내성 리팩터링 시에도 테스트가 정상적으로 동작하는 성질
구현에 밀접하게 의존
상태 검증이 불안정
결과 기반 테스트
구현 디테일 분리
└ 거짓 양성
(허위 경보)
실제로는 문제 없지만 테스트는 실패하는 상황
빠른 피드백 테스트가 빠르게 실행되어야 실시간 피드백이 가능 외부 의존성으로 느려지거나 무거워짐
빠른 단위 테스트 설계
외부 의존성 격리
유지 보수성
가독성, 유지 비용, 실행 비용 등을 평가
   
└ 이해 난이도 테스트가 얼마나 직관적이고 쉽게 읽히는가 복잡한 준비 코드
암시적 의미
AAA 패턴 사용
설명적인 테스트 이름
└ 실행 난이도 테스트가 얼마나 빠르고 독립적으로 실행되는가 외부 리소스 의존성
의존성 분리
Stub/Fake 등 사용

 

예시) 회귀 방지

더보기

// 누군가 실수로 곱셈으로 바꿨음
public class Calculator {
    public int add(int a, int b) {
        return a * b; // ❌ 버그
    }
}
// 테스트가 없거나 add에 대한 검증이 빠짐
// 이 상태에선 테스트가 실패하지 않고 버그가 감지되지 않음 ❌

 

// 기존 기능
public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }
}

// 테스트 (회귀 방지)
public class CalculatorTest {
    @Test
    public void testAdd_twoPositiveNumbers_returnsCorrectSum() {
        Calculator calculator = new Calculator();
        assertEquals(5, calculator.add(2, 3)); // ✅ 회귀 방지
    }
}
  • 이후 add() 로직이 바뀌더라도 이 테스트가 보장해줌.

 

예시) 리팩터링 내성

더보기
public class UserFormatter {
    public String formatUser(User user) {
        return user.getFirstName() + " " + user.getLastName();
    }
}

 

❌  (테스트에서 내부 구현을 지나치게 많이 참조하는 경우)

// 포맷 로직을 캡슐화하거나 외부 위임하게 됨
public String formatUser(User user) {
    return user.getFullName(); // 내부 호출 방식 변경
}
@Test
public void testFormatUser_usesFirstNameAndLastNameDirectly() {
    UserFormatter formatter = new UserFormatter();
    User mockUser = mock(User.class);
    
    when(mockUser.getFirstName()).thenReturn("John");
    when(mockUser.getLastName()).thenReturn("Doe");

    String result = formatter.formatUser(mockUser);

    verify(mockUser).getFirstName();  // ❌ 내부 호출을 검증
    verify(mockUser).getLastName();   // ❌
    assertEquals("John Doe", result);
}
  • 리팩토링 시 내부 구현이 바뀌면 깨짐
  • 구현 세부사항에 의존한 탓

 

✅ (결과만 검증)

@Test
public void testFormatUser_returnsCorrectFullName() {
    User user = new User("John", "Doe");
    UserFormatter formatter = new UserFormatter();

    assertEquals("John Doe", formatter.formatUser(user)); // ✅ 외부 동작만 검증
}
  • 구현 변경이 있어도 테스트가 깨지지 않음

 

예시) 빠른 피드백

더보기
// 외부 API 대신 Fake 객체 사용
public interface PaymentGateway {
    boolean charge(int amount);
}

public class FakePaymentGateway implements PaymentGateway {
    @Override
    public boolean charge(int amount) {
        return true; // 실제 결제 아님
    }
}

public class OrderService {
    private final PaymentGateway gateway;

    public OrderService(PaymentGateway gateway) {
        this.gateway = gateway;
    }

    public boolean placeOrder(int amount) {
        return gateway.charge(amount);
    }
}
// 테스트 (빠르고 독립적)
public class OrderServiceTest {
    @Test
    public void testPlaceOrder_chargesPaymentSuccessfully() {
        OrderService service = new OrderService(new FakePaymentGateway());
        assertTrue(service.placeOrder(100)); // ✅ 빠른 피드백
    }
}
  • 외부 의존성 제거하고 빠르게 동작하도록 구성

 

예시) 유지 보수성

더보기
public class DiscountCalculator {
    public int applyDiscount(int price, int rate) {
        return price - (price * rate / 100);
    }
}
// 명확한 테스트 구조 + SUT/의존성 분리
public class DiscountCalculatorTest {
    @Test
    public void testApplyDiscount_appliesCorrectRate() {
        // Arrange
        DiscountCalculator calculator = new DiscountCalculator();

        // Act
        int discounted = calculator.applyDiscount(1000, 10);

        // Assert
        assertEquals(900, discounted); // ✅ 유지 보수성 높음
    }
}
  • 간결하고 읽기 쉬운 테스트 구조 + 명확한 이름 + 적절한 캡슐화

 

2. 회귀 방지와 리팩터링 내성

개념 설명 관련성
거짓 음성
(False Negative)
테스트가 기능 고장을 잡아내지 못하는 경우
(기능은 고장났으나 테스트는 이를 발견하지 못함)
회귀 방지가 거짓 음성을 방지하는 데 도움
거짓 양성
(False Positive)
테스트가 기능 고장이 아닌 경우를 고장으로 잘못 판단하는 경우
(기능은 정상인데 테스트가 실패하는 경우)
리팩터링 내성이 거짓 양성을 줄이는 데 도움

 

테스트 정확도 향상

  • 회귀 방지와 리팩터링 내성이 모두 테스트 정확도를 높인다.
  • 신호를 증가시켜 버그를 찾아내고, 소음을 줄여 허위 경보를 줄인다.