블라디미르 코리코프 님의 "단위 테스트" 책을 정리한 포스팅입니다.
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());
}
'Code > Test' 카테고리의 다른 글
[단위 테스트] 4-1. 좋은 단위 테스트의 4대 요소: 4대 요소 (0) | 2025.01.19 |
---|---|
[단위 테스트] 3. 단위 테스트 구조 (0) | 2025.01.07 |
[단위 테스트] 2-1. 단위 테스트란 무엇인가: 단위 테스트란 (0) | 2025.01.07 |
[단위 테스트] 1-2. 단위 테스트의 목표: 커버리지 지표 (1) | 2025.01.06 |
[단위 테스트] 1-1. 단위 테스트의 목표: 단위 테스트 목표 (0) | 2025.01.06 |