Closed hcn1519 closed 2 years ago
이 글은 UnitTest를
UnitTest는 흐름
UseCase
setup
, exercise
, verify
, teardown
)SUT Verify - 일반적인 테스트와 동일하게 SUT에 대해서는 Assert를 수행한다.
Mock Verify - Expectation에 명시된 것과 동일한 메소드가 호출되었는지 확인한다.
일반적인 테스트와 주요하게 다른점: Mock은 State Verification이 아닌, Behavior Verification을 사용한다.
System-under-Test(SUT) - 테스트의 주요 대상 객체
Collaborator - SUT를 위해 필요한 주변 객체(Warehouse)
일반적인 테스트 방식은 state verification을 사용한다: exercise method가 정상적으로 동작하였는지 확인하기 위해, SUT와 collaborator의 상태를 확인한다.
진행 단계와 Fixture
전통적인 UnitTest는 4단계 진행 과정으로 수행 됩니다. 각 단계에 대해 간략히 알아보도록 하겠습니다.
Test Double is a generic term for any case where you replace a production object for testing purposes.
테스트 목적으로 만드는 객체 산출물은 모두 Test Double로 부를 수 있습니다.
Test Double is a generic term for any case where you replace a production object for testing purposes.
테스트 목적으로 만드는 객체 산출물은 모두 Test Double로 부를 수 있다.
Testing-oriented people like to use terms like object-under-test or system-under-test to name such a thing. Either term is an ugly mouthful to say, but as it's a widely accepted term I'll hold my nose and use it. Following Meszaros I'll use System Under Test, or rather the abbreviation SUT.
setup
, exercise
, verify
, teardown
)Test Double
)를 정의하여 사용하기도 한다.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.
이번 글에서는 대표적인 Test Double 중 하나인 Stub에 대해 정리합니다.
Stub은 개발자가 테스트하고자 하는 대상(SUT
, System Under Test)에게 indirect input
을 제공하기 위해 사용되는 객체입니다. 대부분의 객체나 함수(Uint)는 많은 경우 주입된 input에 맞추어 output을 리턴하는 형태로 구성됩니다. 그리고, 우리가 작성하는 테스트는 자연스럽게 "내가 주입한 input에 맞추어 output이 나오는가"를 검증합니다. 이 때, 테스트 작성자는 테스트가 Unit을 적절히 검증하는지 확인하기 위해 최대한 다양한 input을 구성합니다. 이러한 경우 제공하는 input을 indirect input이라고 부릅니다. 또한, SUT에 indirect input
을 제공하는 것은 Unit에 직접 주입하는 형태로 구성되기도 하지만, 또 다른 객체에서 제공하기도 합니다. 이 또 다른 객체는 SUT가 의존하는 객체라는 의미에서 DOC(Depended on Component)라고 부르고, Stub은 이 DOC에서 제공하는 input을 대신 제공하는 객체입니다.
SUT
의 행동(behavior)이 input 값에 의존하여 결정될 경우, 이 input을 indirect input
이라고 부릅니다.Stub
은 indirect input
을 제공하는 테스트 목적의 객체입니다.알려진 것처럼 일반적인 테스트는 4단계로 진행됩니다.(setup
, exercise
, verify
, teardown
)
이를 좀 더 자세히 살펴보기 위해 예시를 살펴보겠습니다. 여기서는 사용자의 프로그램 구독을 on/off하는 HTTP API을 처리하는 SubscriptionWorker
를 생각해보겠습니다. 해당 API는 다음과 같은 json 응답을 제공합니다.
{
"subscribed": true,
"subscriptionCount": 27038
}
이 json을 처리하기 위해 다음과 같은 형태로 SubscriptionWorker
를 구성해볼 수 있습니다.
이렇게 구현한 코드는 아래처럼 호출됩니다.
let urlRequest = URLRequest(url: URL(string: "https://hcn1519.github.io")!)
Subscription.Worker.update(request: .init(urlRequest: urlRequest), completion: { result in
// do something
print(result)
})
이 상황에서 우리는 Subscription.Worker.update(request:completion:)
을 테스트하고 싶습니다. 좀 더 정확히는 "HTTP 요청을 통해 획득한 구독 정보를 앱에서 사용 가능한 모델로 잘 전환"되는지에 대한 UseCase를 테스트하고 싶습니다. 이 때, 테스트 작성자는 여러가지 형태의 응답(indirect input
)을 update()
에 제공하여 해당 함수가 올바르게 동작하는지를 확인하고 싶을 수 있습니다. 그런데 문제는 update() 함수는 dataTask()를 호출하므로 외부 서버에 의존적입니다. 즉, 다양한 응답을 구성하려면 서버에서 이를 대응해주어야 합니다.
이런 경우에 Stub
을 사용하면 서버의 도움 없이도 원하는 테스트를 수행할 수 있습니다.
이전의 코드와 변경된 코드의 가장 큰 차이점은 Stub을 주입할 수 있는지 여부입니다. 만약 Stub을 주입하는 것을 활용한다면, update() 함수는 실제 URL에 접근하지 않고, 직접 주입한 StubResponse
를 반환합니다. 즉, 서버에서 원하는 응답을 내려주지 않더라도, Stub을 통해 직접 응답 만들어서 제공할 수 있습니다.
import UIKit
import XCTest
func testSuccess() {
let urlRequest = URLRequest(url: URL(string: "https://hcn1519.github.io")!)
let successData = """
{
"subscribed": true,
"subscriptionCount": 27038
}
""".data(using: .utf8)!
let successURLResponse = HTTPURLResponse(url: urlRequest.url!,
statusCode: 200,
httpVersion: nil,
headerFields: [:])!
let successResponse = Stub.Response(response: successURLResponse,
result: .success(successData))
let successStub = Stub.response(successResponse)
let successRequest = Subscription.Request(urlRequest: urlRequest,
stub: successStub)
Subscription.Worker.update(request: successRequest, completion: { result in
switch result {
case .success(let response):
XCTAssert(response.subscribed == true)
XCTAssert(response.subscriptionCount == 27038)
print("\(#function) success")
case .failure(let error):
XCTAssert(false, "Result should succeed \(error.localizedDescription)")
}
})
}
Note: 여기서는 Stub의 동작 방식을 간결히 보여주기 위해 직접 코드를 작성하였습니다. 이러한 구현은 Moya와 같은 라이브러리에 잘 반영되어 있으니 실제 코드 작성시에는 이를 활용하면 여러모로 편리합니다.
SUT의 indirect input
주입을 제어하기 어려운 경우 - Stub을 활용하면 다양한 indirect input
을 구성하고 이를 통해 SUT의 동작을 제어할 수 있습니다. 예를 들어서 Stub을 사용하여 HTTP 응답을 다양한 성공, 실패 케이스로 구성하면 SUT의 동작이 원하는대로 수행되도록 제어가 가능합니다. 또한, 테스트 환경에서는 접근이 어려운 모듈(e.g. 결제 모듈)을 사용하는 경우, 해당 모듈을 통해 제공받는 값을 Test Stub을 통해 받을 수 있도록 구성하여 test를 수행할 수도 있습니다.
테스트를 통해서 SUT의 동작이 올바른지 검증하는 과정에서 개발자는 크게 State Verification, Behavior Verification 과정을 통해 테스트를 수행할 수 있습니다.
이 때, Mock은 Behavior Verification을 통해 SUT 검증을 진행할 때 사용합니다.
Test Stub xUnit
How It Works
When to Use it
Variation: Test Fixture
In xUnit , a test fixture is all the things we need to have in place in order to run a test and expect a particular outcome. Some people call this the test context .
Variation: Responder
Variation: Saboteur
Variation: Temporary Test Stub
Variation: Entity Chain Snipping
Implementation Note