Spies

Spies는 Test Double 기법 중 하나로, 실제 객체의 메서드 호출을 추적하고 기록하는 데 사용된다.

목적

  1. 메서드 호출 여부, 횟수, 전달된 인자 등을 검증한다.
  2. 실제 구현을 변경하지 않고 메서드의 동작을 관찰한다.
  3. 코드의 상호작용을 분석하고 테스트한다.

장점

  1. 비침투적: 실제 객체의 동작을 변경하지 않고 관찰할 수 있다.
  2. 유연성: 다양한 정보를 수집하고 검증할 수 있다.
  3. 상세한 검증: 메서드 호출의 세부 사항을 정확히 확인할 수 있다.

단점

  1. 복잡성: 과도한 사용 시 테스트 코드가 복잡해질 수 있다.
  2. 오버스펙: 구현 세부사항에 너무 의존적인 테스트를 작성할 위험이 있다.
  3. 성능: 많은 spy를 사용할 경우 테스트 실행 속도가 느려질 수 있다.

예시

예시

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
from typing import Dict, Optional
from datetime import datetime

# 실제 데이터베이스 리포지토리
class UserRepository:
    def __init__(self, database_connection):
        self.db = database_connection
    
    def save(self, user_id: str, user_data: Dict):
        # 실제로는 데이터베이스에 SQL 쿼리를 실행할 것입니다
        self.db.execute(
            "INSERT INTO users (id, data, created_at) VALUES (?, ?, ?)",
            [user_id, user_data, datetime.now()]
        )
    
    def find_by_id(self, user_id: str) -> Optional[Dict]:
        # 실제로는 데이터베이스에서 조회할 것입니다
        result = self.db.execute(
            "SELECT * FROM users WHERE id = ?",
            [user_id]
        )
        return result.fetchone()

# Fake 리포지토리
class FakeUserRepository:
    def __init__(self):
        # 데이터베이스 대신 딕셔너리를 사용
        self.users: Dict[str, Dict] = {}
    
    def save(self, user_id: str, user_data: Dict):
        # 메모리에 직접 저장
        self.users[user_id] = {
            'data': user_data,
            'created_at': datetime.now()
        }
    
    def find_by_id(self, user_id: str) -> Optional[Dict]:
        # 메모리에서 직접 조회
        return self.users.get(user_id)

# 사용자 서비스
class UserService:
    def __init__(self, user_repository):
        self.repository = user_repository
    
    def create_user(self, user_id: str, name: str, email: str):
        user_data = {'name': name, 'email': email}
        self.repository.save(user_id, user_data)
    
    def get_user(self, user_id: str):
        return self.repository.find_by_id(user_id)

# 테스트 코드
def test_user_service():
    # Fake 리포지토리 사용
    fake_repository = FakeUserRepository()
    user_service = UserService(fake_repository)
    
    # 사용자 생성 테스트
    user_service.create_user('user1', 'John Doe', 'john@example.com')
    
    # 사용자 조회 테스트
    user = user_service.get_user('user1')
    assert user['data']['name'] == 'John Doe'
    assert user['data']['email'] == 'john@example.com'

JavaScript

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// 실제 외부 API 서비스
class WeatherService {
    async getTemperature(city) {
        // 실제로는 외부 API를 호출할 것입니다
        const response = await fetch(
            `https://api.weather.com/${city}/temperature`
        );
        return response.json();
    }
}

// Fake 날씨 서비스
class FakeWeatherService {
    constructor() {
        // 미리 정의된 도시별 온도 데이터
        this.temperatureData = {
            'Seoul': { temperature: 25 },
            'New York': { temperature: 20 },
            'London': { temperature: 15 }
        };
    }

    async getTemperature(city) {
        // 실제 API 호출 대신 저장된 데이터 반환
        return Promise.resolve(this.temperatureData[city] || { temperature: 0 });
    }
}

// 날씨 알림 서비스
class WeatherAlertService {
    constructor(weatherService) {
        this.weatherService = weatherService;
    }

    async shouldSendAlert(city) {
        const data = await this.weatherService.getTemperature(city);
        return data.temperature > 30;
    }
}

// 테스트 코드
describe('WeatherAlertService', () => {
    it('should not send alert for normal temperature', async () => {
        // Fake 날씨 서비스 사용
        const fakeWeatherService = new FakeWeatherService();
        const alertService = new WeatherAlertService(fakeWeatherService);
        
        const shouldAlert = await alertService.shouldSendAlert('Seoul');
        expect(shouldAlert).toBe(false);
    });
});

참고 및 출처