네트워크 없이 URLSession을 테스트 하려면 조금 귀찮은 과정들을 거쳐야 한다
네트워크 통신을 했을 때 데이터를 잘 받아오는지는 결과값이나 혹은 어플을 직접 실행해 봐도 확인 할 수 있지만 그 과정에서 던져지는 에러들은 확인하기 어렵다
방대한 양의 테스트를 진행해야 할 경우도 있을텐데 이럴 때 마다 서버를 이용하면 서버에 무리가 갈 수도 있다
와 같은 이유가 있지 않을까?(하고 개인적으로 생각해 본다...)
URLSession의 dataTask는 위와 같이 서버와 통신해서 얻는 결과를 세가지의 값으로 전달 해 준다
즉, 이 과정만 테스트 할 때 바꿔치기 해서 우리가 테스트 하길 원하는 가짜 데이터를 넣어주면 된다는 말!
약간 이런 느낌이지 않을까 싶다
위에서 말했든 dataTask는 Data, Response, Error를 옵셔널 값으로 주는데 이걸 담아 줄 객체를 만들어준다(사실 이건 만들어도 그만 안만들어도 그만이지만 편의를 위해)
struct ResponseResult {
let data: Data?
let response: URLResponse?
let error: Error?
}
protocol URLSessionProtocol {
func customDataTask(request: URLRequest, completion: @escaping (responseResult) -> Void) -> URLSessionDataTask
}
0번에서 정의한 responseResult를 비동기값으로 전해주는 URLSessionDataTask를 반환하는 메서드 생성
이름을 뭐로 해야할지 모르겠어서 일단 customDataTask
라고 만들었다
extension URLSession: URLSessionProtocol {
func customDataTask(request: URLRequest, completion: @escaping (ResponseResult) -> Void) -> URLSessionDataTask {
let dataTask = self.dataTask(with: request) { data, response, error in
let responseResult = ResponseResult(data: data, response: response, error: error)
completion(responseResult)
}
return dataTask
}
}
위와 같이 구현을 했는데 사실 이 부분만 보면 엥? 이거 이렇게 귀찮게 중간다리 하나 더 거칠바에는 그냥 구현하는게 낫지 않겠음? 할 수 도 있다
다시 말하지만 나중에 test할 때는 실질적으로 네트워크와 통신하는 이 부분을 가짜 내용으로 구성할 것이다
struct NetworkManger: NetworkMangerable {
private let session = URLSession.shared
//아래 매개변수로 받는 request는 작성 생략
let dataTask = session.dataTask(with: request) { data, response, error in
DispatchQueue.main.async {
guard error == nil else {
completion(.failure(APIError.transportError))
return
}
guard let response = response as? HTTPURLResponse, (200...299).contains(response.statusCode) else {
completion(.failure(APIError.responseError))
return
}
guard let data = data else {
completion(.failure(APIError.dataError))
return
}
completion(.success(data))
}
}
dataTask.resume()
}
session을 깡으로 생성하고 기본 구현된 dataTask로 서버와의 통신을 하고 있다
struct NetworkManger: NetworkMangerable {
private let session: URLSessionProtocol
init(urlSession: URLSessionProtocol = URLSession.shared) {
self.session = urlSession
}
let dataTask = session.customDataTask(request: request) { result in
DispatchQueue.main.async {
guard result.error == nil else {
completion(.failure(APIError.transportError))
return
}
guard let response = result.response as? HTTPURLResponse, (200...299).contains(response.statusCode) else {
completion(.failure(APIError.responseError))
return
}
guard let data = result.data else {
completion(.failure(APIError.dataError))
return
}
completion(.success(data))
}
}
dataTask.resume()
}
사실 여기까지는 별거 없다 서버 통신도 별다를 것 없이 잘 된다
일단 테스트 관련 파일들은 테스트 폴더에 TestDouble이란 폴더를 만들어 생성했다
final class StubURLSessionDataTask: URLSessionDataTask {
var fakeResume: () -> Void = {}
override func resume() {
fakeResume()
}
}
URLSessionDataTask 클래스를 상속받은 StubURLSessionDataTask를 생성해 준다
여기서 중요한 점은 실제 dataTask의 resume은 서버 통신을 재개하는 역할을 하는데 우리는 통신을 하지 않을 것이기 때문에 URLSessionDataTask에 관한 가짜 객체도 만들어 준다
이 부분도 중요하다(뭐이리 중요한 부분이 많은지 모르겠는데 내가 이 내용들을 이해하는데 너무 오랜시간이 걸려서 그런건지 모르겠지만 다 중요하다....ㅠㅠ)
struct StubURLSession: URLSessionProtocol {
private let dummyData: ResponseResult
init(dummyData: ResponseResult) {
self.dummyData = dummyData
}
/// STEP1
private func stubDataTask(requset: URLRequest, completion: @escaping (ResponseResult) -> Void) -> URLSessionDataTask {
let stubDataTask = StubURLSessionDataTask()
stubDataTask.fakeResume = {
completion(dummyData)
}
return stubDataTask
}
/// STEP2
func customDataTask(request: URLRequest, completion: @escaping (ResponseResult) -> Void) -> URLSessionDataTask {
let dataTask = stubDataTask(requset: request) { result in
completion(result)
}
return dataTask
}
/// STEP3
}
순서대로 살펴보자
우리는 dataTask가 completion으로 전달해 주는 결과 값들이 서버에서 전달 해 주는 값이 아닌 테스트용 데이터를 넣을 것 이기 때문에 StubURLSession을 초기화 할 때 dummyData를 받아 준다
(URLSession의 customDataTask의 responseResult가 아니라 customDataTask 내부의 dataTask의 responseResult의 값이다)
STEP2 구간은 위에 1번단계에서 정의 했던 가짜 dataTask를 만들어주는 메서드 있다
STEP1 에서 초기화시 받은 dummyData를 가짜 dataTask 리줌이 호출되면 completion으로 전달할 수 있도록
즉, 진짜 dataTask가 아니라 가짜 dataTask를 사용하는 테스트코드에서 resume이 호출 되면 여기 작성한 코드들로 인해 dummyData가 completion으로 전달될 것이다
실제 통신할 때 URLSession을 extension 했을 때와 마찬가지로 StubURLSession의 customDataTask메서드를 구현해 준다
실제 통신 때와 다른 점은 실제 통신 때는 URLSession에 있는 진짜 dataTask를 사용했지만
여기서는 STEP2에서 만든 stubDataTask를 사용한다
(다시 말하지만 진짜 dataTask는 resume시 서버 통신을 재개하는 역할을 하지만 stubDataTask는 서버 통신 대신 미리 정의 해둔 dummyData를 completion으로 넘겨준다)
다만...
문제점이 하나가 있다면
URLSession을 초기화 하는게 13.0부터 deprecated됐다고 하는데 이거 해결법 아시는분...?
일단 test코드이기 때문에 넘어가긴 하지만 해결하고 싶다...😭😭
func test_netWorkHandler의호출시_statusCode가_불안정할경우_responseError를_잘던지는지() {
//given
let promise = expectation(description: "responseError와 일치하는지")
let dummyData = makeDummyData(data: Data(), statusCode: 404, error: nil)
let netWorkHandler = NetworkManger(urlSession: StubURLSession(dummyData: dummyData))
let testAPIModel = TestAPIModel(bookTitle: "", host: "", path: "", method: .get)
//when
netWorkHandler.request(api: testAPIModel) { data in
switch data {
case .success(_):
XCTFail()
case .failure(let error):
//then
XCTAssertEqual(error, APIError.responseError)
}
promise.fulfill()
}
wait(for: [promise], timeout: 5)
}
요런식으로 작성했다
참고 자료
https://sujinnaljin.medium.com/swift-mock-%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-network-unit-test-%ED%95%98%EA%B8%B0-a69570defb41
https://wody.tistory.com/10