블라디미르 코리코프 님의 "단위 테스트" 책을 정리한 포스팅입니다.
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());
}
'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 |