Code/Test

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

noahkim_ 2025. 1. 7. 11:40

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

 

2. 런던파와 고전파

  • 격리 방식에 따라 크게 두 가지 스타일로 나뉨
항목 런던파 (London School)
고전파 (Classical School)
중심 철학 상호작용 기반 (행위 검증)
상태 기반 (결과 검증)
테스트 단위 코드 조각
단일 동작
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());
}

 

Mock vs Stub/Fake

항목 Mock Stub / Fake
목적 동작 검증 (행위 중심) 동작 대체 (예상된 값 반환)
동작 검증 ✅ (호출 여부, 인자, 횟수 등)
사용 목적 협력자 호출 확인이 중요한 경우
외부 의존성 제거 (빠르게 예상 값 반환)
예시 구분 verify()로 호출 검증
결과 assertEquals()로 비교

 

예제) 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 응답 대체 메모리 DB, 임시 저장소 등
검증 방식 결과 값 상태 변화, 데이터 흐름

 

예제) 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());
}