Code/Test

[단위 테스트] 2-2. 단위 테스트란 무엇인가: 런던파와 고전파

noahkim_ 2025. 1. 7. 11:40

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

 

2. 런던파와 고전파

  • 격리 특성에 따라 크게 두 가지 스타일로 나뉨
항목 런던파 (London School)
고전파 (Classical School)
테스트 단위 작은 코드 조각 단위
단일 동작 단위 중심
TDD 방식 하향식 TDD
– 전체 설계/인프라 → 상위 → 하위 구현
상향식 TDD
– 작은 단위 → 상위 통합
의존성 처리 모든 의존성을 Mock으로 대체 (불변 비공개 제외)
공유 의존성만 Fake/Stub으로 격리 (나머지는 실제 객체 사용)
중심 철학 상호작용 기반 테스트
- 협력자와의 상호작용을 테스트
상태 기반 테스트
- 결과를 중심으로 동작 검증
장점 - 문제 위치 파악 쉬움 (테스트 입자성 높음) - 신뢰도 높은 테스트 가능 (실제 객체와의 상호작용)
- 현실적인 테스트 작성 가능
단점 - 과잉 명세
- 구현 의존성 높음
- 문제 발생 시 원인 추적이 어려움

 

예제) EmailSender

더보기

런던파

// 인터페이스
public interface EmailSender {
    void sendEmail(String to, String body);
}

// 실제 서비스
public class UserService {
    private final EmailSender emailSender;

    public UserService(EmailSender emailSender) {
        this.emailSender = emailSender;
    }

    public void register(String email) {
        // 사용자 등록 로직 생략
        emailSender.sendEmail(email, "Welcome!");
    }
}

// 테스트 (런던파 스타일)
@Test
void register_shouldSendWelcomeEmail() {
    EmailSender mockSender = Mockito.mock(EmailSender.class);
    UserService userService = new UserService(mockSender);

    userService.register("test@example.com");

    Mockito.verify(mockSender).sendEmail("test@example.com", "Welcome!");
}

 

고전파

// 실제 구현
public class WelcomeEmailSender implements EmailSender {
    private String lastSentTo;

    @Override
    public void sendEmail(String to, String body) {
        // 실제 이메일 발송 생략
        this.lastSentTo = to;
    }

    public String getLastSentTo() {
        return lastSentTo;
    }
}

// 테스트 (고전파 스타일)
@Test
void register_shouldSendEmailToCorrectUser() {
    WelcomeEmailSender fakeSender = new WelcomeEmailSender();
    UserService userService = new UserService(fakeSender);

    userService.register("classic@example.com");

    assertEquals("classic@example.com", fakeSender.getLastSentTo());
}

 

항목 런던파 고전파
TDD 방식 하향식
- 상위 수준부터 테스트하고, 하위 컴포넌트를 Mock으로 대체
상향식
- 세부 구현부터 테스트 작성, 점차 상위 레벨로 통합함
의존성 처리 모든 협력자를 Mock으로 격리
공유 의존성만 Stub/Fake로 처리
중심 철학 협력자와의 상호작용 검증
메서드 실행 후 상태 검증

 

 

Mock vs Stub/Fake

카테고리 Mock Stub/Fake
목적 동작 검증
(어떤 메서드가 호출되었는지, 호출 횟수 등)
동작 대체
(특정 동작을 흉내내거나 미리 정의된 결과 반환)
사용 주체 실제 동작을 검증하고자 할 때
의존성을 대체하고 결과를 미리 정의할 때
상태 변경 테스트 목적에 맞게 상태를 검증 테스트에서 필요로 하는 특정 결과를 반환 (실제 동작 ❌)
동작 검증 어떤 메서드가 호출되었는지
몇 번 호출되었는지
어떤 인자가 전달되었는지
미리 정의된 결과를 반환 (동작을 검증하지 않음)
구현 방식 동작 검증을 위한 verify 기능 제공
단순히 결과 대체를 위해 사용되는 간단한 구현

 

예제) Mock

더보기
// 의존성 클래스 B
class B {
    public void doSomething() {
        // 실제 동작
    }
}

// 클래스 A
class A {
    private B b;

    public A(B b) {
        this.b = b;
    }

    public void process() {
        b.doSomething();  // B의 doSomething 메서드를 호출
    }
}
public class MockTest {
    @Test
    void testProcessCallsDoSomething() {
        // B를 Mock 객체로 생성
        B mockB = mock(B.class);
        A a = new A(mockB);

        // A의 process 메서드 실행
        a.process();

        // doSomething 메서드가 한 번 호출되었는지 확인
        verify(mockB, times(1)).doSomething();  // 검증
    }
}

 

예제) Stub

더보기
// 의존성 클래스 B
class B {
    public String fetchData() {
        return "Real Data";  // 실제 동작
    }
}

// 클래스 A
class A {
    private B b;

    public A(B b) {
        this.b = b;
    }

    public String getData() {
        return b.fetchData();  // B의 fetchData 메서드 호출
    }
}
// Stub 구현
class StubB extends B {
    @Override
    public String fetchData() {
        return "Mocked Data";  // 미리 정의된 값을 반환
    }
}

public class StubTest {
    @Test
    public void testGetDataReturnsMockedValue() {
        // Stub 객체 생성
        B stubB = new StubB();
        A a = new A(stubB);

        // A의 getData 메서드 실행
        String result = a.getData();

        // 예상된 값과 비교
        assertEquals("Mocked Data", result);  // 결과 검증
    }
}

 

Stub vs Fake

구분 Stub Fake
목적 외부 의존성에서 미리 정의된 값을 반환 (간단한 대체)
실제 시스템을 대신하여 동작을 간단히 구현
사용 예 API 호출을 모방하여 특정 값만 반환
실제 데이터베이스나 서비스 대신 간단한 구현으로 대체
검증 결과 값만 검증 (상호작용이나 호출 횟수는 검증 안 함)
동작과 결과를 모두 검증 (동작을 간단하게 구현)

 

예제) Stub

더보기
// 의존성 클래스 B (실제 DB 호출 대신 Stub 사용)
class Database {
    public String getUserData(String userId) {
        return "Real User Data";  // 실제 DB에서 데이터를 가져오는 메서드
    }
}

// 테스트에서 사용할 Stub 클래스
class StubDatabase extends Database {
    @Override
    public String getUserData(String userId) {
        return "Stubbed User Data";  // Stub은 실제 데이터 대신 미리 정의된 값을 반환
    }
}

// 클래스 A
class UserService {
    private Database database;

    public UserService(Database database) {
        this.database = database;
    }

    public String getUser(String userId) {
        return database.getUserData(userId);  // Stub 객체의 반환 값 사용
    }
}
@Test
public void testGetUserReturnsStubbedData() {
    // Stub 객체 생성
    Database stubDatabase = new StubDatabase();
    UserService userService = new UserService(stubDatabase);

    // UserService의 getUser 메서드 실행
    String result = userService.getUser("user123");

    // Stubbed 데이터 검증
    assertEquals("Stubbed User Data", result);
}

 

 

예제) Fake

더보기
// 의존성 클래스 B (실제 DB 연결 대신 Fake 데이터베이스)
class Database {
    private Map<String, String> data = new HashMap<>();

    public void saveUserData(String userId, String userData) {
        data.put(userId, userData);  // 데이터를 저장하는 메서드
    }

    public String getUserData(String userId) {
        return data.get(userId);  // 데이터를 반환하는 메서드
    }
}

// FakeDatabase 클래스 수정
public class FakeDatabase implements Database {
    private boolean getCalled = false;

    @Override
    public String getUserData(String userId) {
        getCalled = true;  // get이 호출되었음을 추적
        return "Fake User Data";  // Fake 데이터 반환
    }

    public boolean isGetCalled() {
        return getCalled;  // get이 호출되었는지 확인하는 메서드
    }
}


// 클래스 A
class UserService {
    private Database database;

    public UserService(Database database) {
        this.database = database;
    }

    public String getUser(String userId) {
        return database.getUserData(userId);  // Fake 객체에서 데이터 가져오기
    }
}
@Test
public void testGetUserReturnsFakeData() {
    // Fake 객체 생성
    FakeDatabase fakeDatabase = new FakeDatabase();
    UserService userService = new UserService(fakeDatabase);

    // UserService의 getUser 메서드 실행
    String result = userService.getUser("user123");

    // Fake 데이터 검증
    assertEquals("Fake User Data", result);

    // 동작 검증: get 메서드가 호출되었는지 확인
    assertTrue(fakeDatabase.isGetCalled());
}