이번 포스팅에서는 테스트더블에 대해서 다뤄보겠습니다.

Test Double이란?

  • 테스트를 진행할 때, 실제 객체를 대신에 사용하는 객체를 Test Double(테스트더블) 이라고 함
  • 테스트 더블은 실제 객체와 같은 인터페이스를 사용하여 구현
  • 테스트 더블을 사용하는 클라이언트는 해당 객체가 테스트 더블인지 실제 객체인지 알 수 없음

Test Double 종류

  • Dummy
  • Fake
  • Stub
  • Mock
  • Spy

Dummy

더미란 실제 사용되지는 않지만 필요한 객체입니다.

class A {
    func function() { }
}

class B {
    func function() { }
}

class C {
    var a: A
    var b: B
    
    init(a: A, b: B) {
        self.a = a
        self.b = b
    }
    
    func functionB() {
        b.function()
    }
}

C객체의 functionB함수를 테스트하기 위해서는 C객체를 생성해야 할때 A와 B객체를 주입해야하는데 실제로 A객체는 사용되지 않습니다. 하지만 테스트를위해 A객체는 주입되어야 하죠 이런경우 A를 더미 객체로 생성해 주입합니다.

Fake

Fake는 실제 구현이 존재하는 객체로 실 환경에서는 사용할 수 없지만, 테스트를 위해서 사용하는 객체입니다.

protocol Sum {
	func function(lhs: Int, rhs: Int)-> Int
}

class A: Sum {
    func function(lhs: Int, rhs: Int)-> Int {
        print(lhs + rhs) // Logger
        return lhs + rhs
    }
}

class FakeA: Sum {
    func function(lhs: Int, rhs: Int)-> Int {
    	return lhs + rhs
    }
}

동일한 Sum프로토콜을 채택하는 FakeA객체를 생성할 때 실제 프로덕트환경에서 사용되는 로직을 제거하고 테스트가 필요한 로직만 남겨둔 채 구현합니다. 이 외에 FakeDB, FakeWebService등 실제 환경에서 DB나 외부서버에 접근하는것은 불필요하며 테스트 속도를 저하시키다 보니 이런 외부적은 요소들을 Fake객체로 구현하기도 합니다.

Stub

하드코딩된 정해진 결과값을 반환하는 객체입니다.

protocol NetworkService {
    func request(completion: @escaping ((String)->Void)?)
}

class NetworkProvider: NetworkService {
    func request(completion: @escaping ((String)->Void)?) {
        let url = URL(string: "url")!
        let session = URLSession.shared
        
        let task = session.dataTask(with: url) { data, response, error in
            guard
                let data = data,
                let result = String(data: data, encoding: .utf8)
            else {
                return
            }
            // handler 사용
            completion?(result)
        }
    }
}

class NetworkProviderStub: NetworkService {
    func request(completion: ((String) -> Void)?) {
        completion?("MockData")
    }
}

구현된Stub은 실제 통신의 결과가 아닌 하드코딩된 데이터를 반환하도록 구현돼있다. 이처럼 네트워크 환경에 의존하지 않고 네트워크 통신 로직을 테스트 할 수 있다.

Mock

Mock은 행위를 검증하기위한 객체이다. 내가 작성한 로직이 정상적으로 실행 됐는지 여부를 판단한다.

class NetworkProviderMock: NetworkService {
	var count = 0
    func request(completion: ((String) -> Void)?) {
		count += 1
    }
}

Stub과 다른점은 count값을 통해 해당 로직이 실행됐는지 여부를 판단한다.

Spy

Spy는 Stub의 역할을 가지면서 호출된 내용에 대해 약간의 정보를 기록한다. 나는 Spy를 Stub + Mock 라고 이해했다.

class NetworkProviderMock: NetworkService {
	var count = 0
    func request(completion: ((String) -> Void)?) {
		completion?("MockData")
		count += 1
    }
}

Stub의 기능을 수행하면서 별도로 필요한 정보들을 기록하며 해당 로직이 정상적으로 실행됐는지 테스트한다. 결국 Mock역할도 수행이 가능하다. 

마치며

어느분의 조언으로 테스트더블에 대해 알게된 후 작성해봤는데 요즘은 Testcode를 통해 안전한 로직을 구현하는것이 중요한 역량이라고 생각됩니다. TestDoubled을 통해 적절한 시점에 적잘한 객체를 구현해 사용하면 TestCode를 작성하는데 좀 더 수월하다고 생각합니다.

Reference

http://xunitpatterns.com/Test%20Double%20Patterns.html

https://velog.io/@dlsxor21c/iOS와-Test-double

https://velog.io/@ljinsk3/Mock-테스트-feat.-Test-Double

https://jiseobkim.github.io/swift/2022/02/06/Swift-Test-Double(부제-Mock-&-Stub-&-SPY-이런게-뭐지-).html 

 

복사했습니다!