TIL
지금까지 API 유닛테스트를 한 방법은 실제 API 요청을 통해 수행했었다. 그런데 몇몇 개발자 블로그를 찾아보니 실제 네트워킹 요청을 하게되면 테스트가 오래걸리고, 특히 post요청은 실제 서버의 프로덕션 환경을 오염시킬 수 있다는 좋지 않은 방법이라고 설명이 되어있다.. 그래서 나의 테스트방법을 바꿔보기 위해 찾아보게 되었다.
protocol URLSessionProtocol {
func data(
for request: URLRequest,
delegate: URLSessionTaskDelegate?
) async throws -> (Data, URLResponse)
}
내가 사용하는 URLSession.shared.data(for:)
메서드와 같은 정의로 만들어 준다.
extension URLSession: URLSessionProtocol {}
class MockURLSession: URLSessionProtocol {
typealias Response = (data: Data, urlResponse: URLResponse)
let response: Response
init(response: Response) {
self.response = response
}
func data(for request: URLRequest, delegate: URLSessionTaskDelegate? = nil) async throws -> (Data, URLResponse) {
return (response.data, response.urlResponse)
}
}
이후 이 프로토콜을 실제 URLSession 에도 채택 시키고, 새로운 MockURLSession 에 채택하여 만들어준다.
MockURLSession 은 API 요청시 테스트용으로 쓰일 URLSession 이다.
struct APIClient {
static let shared = APIClient(sesseion: URLSession.shared)
private let session: URLSessionProtocol
init(sesseion: URLSessionProtocol) {
self.session = sesseion
}
func requestData(with urlRequest: URLRequest) async throws -> Data {
let (data, response) = try await session.data(for: urlRequest, delegate: nil)
let successRange = 200..<300
guard let statusCode = (response as? HTTPURLResponse)?.statusCode else { throw APIError.unknown }
guard successRange.contains(statusCode) else { throw APIError.response(statusCode) }
return data
}
}
기존에 URLSession 타입 -> URLSessionProtocol 타입으로 변경하여 URLSession.shared 와 MockURLSession 둘다 들어올 수 있도록 만든다.
func test_statusCode가_500일때_APIError_response를_반환하는지() async throws {
// given
let mockResponse: MockURLSession.Response = {
let data: Data = Data()
let successResponse = HTTPURLResponse(
url: URL(string: "testURL")!,
statusCode: 500,
httpVersion: nil,
headerFields: nil
)!
return (data: data, urlResponse: successResponse)
}()
let mockURLSession = MockURLSession(response: mockResponse)
let sut = APIClient(sesseion: mockURLSession)
let expectation = APIError.response(500).errorDescription
// when
do {
_ = try await sut.requestData(with: URLRequest(url: URL(string: "testURL")!))
} catch let error as APIError {
// then
XCTAssertEqual(error.errorDescription, expectation)
}
}
이렇게 mockResponse 를 만들어 원하는 반환값을 설정해줄 수 있다. async 함수인 URLSession.shared.data(for:) 메서드는 반환값이 (data, response) 이므로 이에 맞게 설정 해주었다.
첫번째로 statusCode가 500일 경우 오류처리를 잘 하는지 테스트를 해보았다. (Data 는 그냥 아무렇게나 넣어주었다. 어차피 response에서 throw 를 할테니..)
잘 통과한다 ! 이제 실제 resopnse의 statusCode 가 200~300 범위를 벗어날 때 오류가 정상적으로 출력됨을 보장받을 수 있다.
Response 에서 Data 도 설정해줄 수 있었다. 반환되는 Data 값이 만약 ResponseType 과 다를경우에 대한 오류처리 테스트를 할 수 있다.
struct MockAPI: API {
typealias ResponseType = ProductListResponseDTO
let configuration: APIConfiguration?
init() {
configuration = APIConfiguration(
method: .get,
base: "testURL",
path: "",
parameters: nil
)
}
}
내가 사용하는 API를 테스트 해볼 것이다. ProductListResponseDTO 타입의 response를 가지는 MockAPI 를 만들어 준다. 이 타입은 execute를 할 때, decoding할 타입으로 사용 된다. ▼ (API 구현부이다)
protocol API {
associatedtype ResponseType: Decodable
var configuration: APIConfiguration? { get }
}
extension API {
func execute(using client: APIClient = APIClient.shared) async throws -> ResponseType {
guard let urlRequest = configuration?.makeURLRequest() else { throw APIError.invalidURL }
let data = try await client.requestData(with: urlRequest)
if ResponseType.self == String.self {
let result = String(data: data, encoding: .utf8)!
return result as! Self.ResponseType
}
do {
let result = try JSONDecoder().decode(ResponseType.self, from: data)
return result
} catch {
throw APIError.failToParse
}
}
}
그리고 MockResponse의 data 타입을 ProductListResponseDTO가 아닌, 다른 타입을 반환하도록 만들어준다. 즉, 우리가 적용해둔 반환타입과 실제 반환타입이 다른 경우를 테스트 하는 것이다.
func test_JSONDecoding이_실패했을때_적절한오류를_반환하는지() async throws {
// given
let mockResponse: MockURLSession.Response = {
let data: Data = try! JSONEncoder().encode("abc")
let successResponse = HTTPURLResponse(
url: URL(string: "testURL")!,
statusCode: 200,
httpVersion: nil,
headerFields: nil
)!
return (data: data, urlResponse: successResponse)
}()
let mockURLSession = MockURLSession(response: mockResponse)
let apiClient = APIClient(sesseion: mockURLSession)
let api = MockAPI()
let expectation = APIError.failToParse.errorDescription
// when
do {
_ = try await api.execute(using: apiClient)
XCTFail()
} catch let error as APIError {
// then
XCTAssertEqual(error.errorDescription, expectation)
}
}
MockURLSession 을 만들어 테스트 하는 방법을 직접 사용해보니 어떤 응답이 주어졌을 때, 내가만든 네트워킹 객체가 응답을 적절하게 처리를 해주는가(특히 오류처리를 잘 하는가)에 대한 테스트를 가능케 한 것 같다.