🧪 Test Double?
항상 테스트를 하면서 Mock, Stub, Fake... 이런 키워드들을 보면서 언제 어떤 걸 쓰는 게 맞는 표현인지가 궁금해서 이번기회에 제대로 찾아보게 되었다.
🤓 공식... 정의?
솔직히 요즘 인공지능이 생겨나면서 인터넷에서 도대체 어떤것이 정확한 자료인지 항상 고민이 많이 된다. 그러다가 마틴 파울러 사이트에서 이것을 명확하게 정의한 것을 보게 되었다. 마틴 파울러가 누군지는 굳이 말할 필요 없을 것 같다.
bliki: Test Double
Test Double is generic term for fakes, mocks, stubs, dummies and spies.
martinfowler.com
마틴 파울러는 5가지를 소개하고 있다. Dummy, Fake, Stub, Spy, Mock이다. 하나하나 알아보자
📦 Dummy
Dummy objects are passed around but never actually used. Usually they are just used to fill parameter lists.
사용되지 않는 그냥 채우기만을 위한 데이터를 Dummy라고 한다. 테스트에서 단지 파라미터에 들어가기만 하는 데이터들을 더미라고 한다.
🎠 Fake
Fake objects actually have working implementations, but usually take some shortcut which makes them not suitable for production (an InMemoryTestDatabase is a good example).
실제 동작하지만 아주 단순화된 테스트용 구현체...? 정도로 생각하면 될 것 같다. 예를 들면 In Memory Test Database처럼 디스크에 저장해야 하는 Database를 앱이 구동되는 동안만 저장한다던지...
💼 Stub
Stub provide canned answers to calls made during the test, usually not responding at all to anything outside what's programmed in for the test.
정해진 응답을 제공한다. 외부 의존성이 없다.
🕸️ Spies
Spies are stubs that also record some information based on how they were called. One form of this might be an email service that records how many messages it was sent.
사실 이친구가 제일 이해가지 않았는데 Stub처럼 미리 정해진 응답을 주고, 호출된 정보를 기록한다고 한다. Log가 달려있는 느낌인가...?
🧫 Mock
Mocks are pre-programmed with expectations which form a specification of the calls they are expected to receive. They can throw an exception if they receive a call they don't expect and are checked during verification to ensure they got all the calls they were expecting.
테스트 과정에서 사전에 프로그래밍된 객체로, 호출된 메서드와 파라미터를 검증한다.
🧐 나의 정리
확실한 건 이 글은 2006년에 작성된 글이라는 것이다. 🫠 그리고 읽어봐도 음... 명확하게 구분이 힘들다고 생각했다. Dummy나 Spies, Fake는 어느 정도 알겠는데 역시나 Stub과 Mock의 차이가 쉽지 않았다.
그것과 연관되어 Mocks Aren't Stubs라는 아티클이 있다.
Mocks Aren't Stubs
Explaining the difference between Mock Objects and Stubs (together with other forms of Test Double). Also the difference between classical and mockist styles of unit testing.
martinfowler.com
이곳에서 아래와 같이 설명하고 있다.
This difference is actually two separate differences. On the one hand there is a difference in how test results are verified: a distinction between state verification and behavior verification.
Stub은 상태를 검증하고, Mook은 메서드의 동작 자체를 검증한다.
Unit Testing: Exploring The Continuum Of Test Doubles
Article 10/02/2019 In this article --> Unit Testing Exploring The Continuum Of Test Doubles Mark Seemann Code download available at:Testing2007_09.exe(271 KB) This article discusses: Unit testing with test doubles Dummies, stubs, spies, fakes, and mocks Ma
learn.microsoft.com
이곳에서 조금더 깔끔하게 정리한 느낌이 들었다? 얼마나 구현이 되어있냐에 따라 생각해 봐도 좋을 것 같다.
- Dummy: 자리 채우기용 객체, 구현 없음.
- Stub: 최소 구현체로 하드코딩된 값을 반환.
- Spy: 스텁처럼 동작하며, 메서드 호출 정보를 기록.
- Fake: 실제 구현을 흉내 내되, 성능 등을 위해 간소화된 객체.
- Mock: 동적으로 생성되며, 반환값 설정과 호출 검증 기능을 제공.
다양한 프레임워크에서 Mock라이브러리를 지원한다고 한다. 왜 Swift에는 없을까? 를 찾아봤는데 우선 이것도 좀 깊은 내용인 것 같아서 우선 Swift는 readwrite reflection을 허용하지 않는데, 런타임 도중 다른 요소가 코드를 수정할 수 없게 만들어서 안정성을 더했다 정도로 생각해 보면 될 것 같다.
결국 돌고돌아 조금 더 넓은 범위, 특히 행위에 많은 초점을 두었다면 Mock객체, 하드코딩 된 값을 반환만 한다면 Stub이 맞는 것 같다. 그런 의미에서 내가 작성한 MockURLProtocol은 Mock이 맞는가? 생각해보면 사실 MockURLProtocol은 SUT의 협력자이다. 행동보다는 하드코딩된 데이터를 반환하지만, url에 따라 유동적으로 값을 변경하니 Mock으로 봐도 괜찮을 듯 싶다. 이렇게 보니 Fake같기도...? 그래도 HTTPURLResponse, Data를 테스트에서 검증 하므로 Mock이 맞다는 판단을 내리기로 했다!
final class MockURLProtocol: URLProtocol {
override class func canInit(with request: URLRequest) -> Bool { true }
override class func canonicalRequest(for request: URLRequest) -> URLRequest { request }
override func startLoading() {
do {
let (data, response) = try data(for: request)
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
client?.urlProtocol(self, didLoad: data)
client?.urlProtocolDidFinishLoading(self)
} catch {
client?.urlProtocol(self, didFailWithError: error)
}
}
override func stopLoading() { }
}
extension MockURLProtocol {
private func data(for request: URLRequest) throws -> (Data, HTTPURLResponse) {
guard let url = request.url, let query = url.query() else { throw URLError(.unknown) }
let data: [String: Data] = [
"key": stubData
]
if let data = data.first(where: { query.contains($0.key) }) {
return (data.value, response(from: url))
} else {
return (Data(), response(from: url, statusCode: 404))
}
}
private func response(from url: URL, statusCode: Int = 200) -> HTTPURLResponse {
let badResponse = HTTPURLResponse(url: url, statusCode: statusCode, httpVersion: nil, headerFields: nil)
return badResponse ?? HTTPURLResponse()
}
}
😇 마치며
그래도 이렇게 알아보면서 누가 이거 왜 Mock이라고 했어요? 라고 물어본다면 그래도 주저리주저리 답할 수 있을 정도가 된 것 같아 기쁘다... 테스트의 경험 자체가 적다 보니 이런 용어의 정의를 봐도 아 그거구나! 바로 연상이 안 되는 느낌도 있다. 테스트를 많이 해봐야겠다.
마틴 파울러는 Mocks Aren't Stubs에서 Classical TDD, Mockist TDD에 대한 말이 있었다. Classical TDD는 실제 객체를 최대한 가지고 테스트하는 방식, Mockist TDD는 테스트하는 객체를 제외하고 모두 Mock으로 만들어 테스트하는 방식을 말하고 있다.
지금의 XCTest, Testing에서는 나는 최대한 실제 객체를 가지고 테스트를 할 수 있도록 만들고 싶다. URLProtocol과 같이 어쩔 수 없는 부분이 있는 경우만 Mock, Stub을 활용하고자 한다. (캐싱도... Fake 써야 할 것 같다) 이번에 개인프로젝트를 하면서 기존의 기능을 어떻게 보장할 수 있는가에 대한 생각과 실제 빌드를 하며 테스트하는 것보다 테스트 코드를 통해 명확하게 검증하는 것이 더 안전하다고 느꼈고, 많은 기능들을 테스트화 해보면서 TDD에 가까워지고 싶다.
솔직히 Preview로 보는 거 아닌 이상 코드를 작성하면서 이거 되나...? → 시뮬레이터나 실기기 빌드 후 → 된다!!! 이런 과정이었는데, 이거 되나? 싶을 땐 테스트코드를 작성하는 습관을 들어야겠다.
'📺 Programming' 카테고리의 다른 글
[Programming] 소프트웨어 아키텍쳐, 디자인 패턴 (0) | 2024.01.06 |
---|