본문 바로가기

Computer

Test Double 특징

다음과 같이 구조체와 프로토콜이 존재할 때,

struct Post {
    let id: UUID
    let title: String
}

protocol PostRepositoryProtocol {
    
    var postCount: Int { get }
    
    func savePost(_ post: Post) throws -> Bool
    
    func getPost(by id: UUID) throws -> Post
    
}

 

 

Dummy

- 매개변수 목록을 채우기 위한 빈 객체

- 사용되지 않을것이므로 프로토콜 요구 사항만 충족

final class PostRepositoryDummy: PostRepositoryProtocol {
    
    var postCount: Int = 0 // 사용되지 않음
    
    // 호출되지 않음
    func savePost(_ post: Post) throws -> Bool {
        return true // true or false 반환값이 의미 없다.
    }
    
    // 호출되지 않음
    func getPost(by id: UUID) throws -> Post {
        return .init(id: .init(), title: "") // 의미 없음
    }
    
}

 

 

Stub

- 실제로 동작하는 것처럼 보이게 구현

- 최소한의 구현으로 미리 프로그래밍된 응답만 수행

final class PostRepositoryStub: PostRepositoryProtocol {
    
    var postCount: Int = 5 // 테스트 도중 사용될 수 있는 임의의 값
    
    func savePost(_ post: Post) throws -> Bool {
        return true // 미리 프로그래밍한 임의의 응답
  // or return false
  // or throw SomeError()
    }
    
    func getPost(by id: UUID) throws -> Post {
        return .init(id: .init() ,title: "Types of Test double") // 임의의 응답
  // or throw SomeError()
    }
    
}

 

 

Fake

- 논리에 맞게 실제로 작동하는 객체

- 최대한 간단하게 구현할 수 있는 방법을 사용(실제 Product 에 적합하지 않더라도)

final class PostRepositoryFake: PostRepositoryProtocol {
    
    // 테스트 동안 실제로 동작하는 것처럼 보이도록 임의로 추가
    // 하지만 실제 Product 에서는 DB 등의 저장소를 연결
    var postStorage: [Post] = []
    
    var postCount: Int { postStorage.count } // 저장된 post 갯수를 정확하게 반환
    
    func savePost(_ post: Post) throws -> Bool {
        postStorage.append(post) // Post 객체를 실제로 저장
        return true
    }
    
    func getPost(by id: UUID) throws -> Post {
    	// 저장된 객체를 실제로 검색해서 있으면 반환, 없으면 예외를 던진다.
        guard let post = postStorage.first(where: { $0.id == id }) else {
            throw SomeError()
        }
        return post
    }
    
}

 

 

Spy

- 메소드의 호출, 객체의 변화 등 테스트 검증에 필요한 정보를 일부 기록하는 객체

final class PostRepositorySpy: PostRepositoryProtocol {
    
    var postCount: Int = 0 // savePost(_:) 함수가 호출된 횟수를 기록하는 용도로 사용됨
    
    var fetchedCount: Int = 0 // getPost(by:) 함수가 호출된 횟수를 기록하는 용도로 사용됨
    
    func savePost(_ post: Post) throws -> Bool {
        postCount += 1 // 함수 호출 횟수 기록
        return true
    }
    
    func getPost(by id: UUID) throws -> Post {
        fetchedCount += 1 // 함수 호출 횟수 기록
        return .init(id: .init() ,title: "Types of Test double") // 임의의 응답
    }
    
}

 

 

Mock

- 엄밀하게는 기대 되는 행동(함수의 호출 등)을 정의하고 스스로 검증(verify)하는 객체

- 일반적으로 행위 검증을 위한 객체

 

 

 

특징 비교 표

  테스트 도중 사용 여부 실제 정상 작동 여부 상태 검증에 적합 행위 검증에 적합
Dummy X X X X
Stub O X X(1) X
Fake O O O X
Spy O △(2) △(3) X
Mock O O(4) X O

 

(1) 엄밀한 의미의 Stub 은 최소한의 구현만 존재하기 때문에 상태가 변화하지 않음 -> 상태 검증 불가

(2) 정보를 기록한다고 해서 실제로 작동하는 객체인지 보장할 수 없음 (실제로 작동하도록 구현 가능)

(3) 실제 작동이 보장되지 않으므로 상태 변화도 보장되지 않음 (실제로 작동하고 상태가 변화하도록 구현 가능)

(4) 예상한 기대치에 맞게 동작함을 보장해야 하므로 실제로 작동하는 객체 또는 실제로 작동하는 것처럼 보이는 객체

 

 

Test Double 명칭들에 대한 인식

[Fake, Spy] 는 일반적으로 엄밀하게 구분하지 않고 보통 [Dummy, Stub, Mock] 으로 구분하는것 같다.

 

오직 매개변수를 채우는 용도로 만든 구현체는 Dummy,

행위 검증을 위해 기대치를 정의하고 자체 검증을 수행할 수 있는 객체는 Mock,

그 외 나머지 간단한 구현이나 상태 확인을 위해 추가 메소드가 구현되어 있는 객체들은 Stub 의 확장이라고 부를 수 있겠다.