Code/Test

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

noahkim_ 2025. 1. 19. 03:29

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


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

요소 설명 문제 상황 / 원인 해결 방안
회귀 방지 코드 수정 후 기능이 깨지는 것을 방지하는 테스트 역할 코드 수정 후 테스트는 통과하지만 실제로는 동작하지 않음
기존 동작을 명확히 검증
리팩터링 내성 코드 내부 구현이 변경되어도 테스트가 정상적으로 동작해야 함
리팩토링 시 내부 동작 테스트 깨짐
결과 기반 테스트
구현 디테일 분리
빠른 피드백 테스트가 빠르게 실행되어야 실시간 피드백이 가능 외부 의존성으로 느려지거나 무거워짐
의존성 분리 (Fake/Stub)
유지 보수성
가독성, 유지 비용, 실행 비용 등을 평가
   
└ 이해 난이도 테스트가 얼마나 직관적이고 쉽게 읽히는가 복잡한 준비 코드
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. 거짓 음성 vs 거짓 양성

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

 

테스트 정확도 향상

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