[iOS] Async/await 네트워킹 유닛테스트 시도하기

Wongbing·2023년 1월 30일
0

Skills

목록 보기
8/8
tags: TIL

지금까지 API 유닛테스트를 한 방법은 실제 API 요청을 통해 수행했었다. 그런데 몇몇 개발자 블로그를 찾아보니 실제 네트워킹 요청을 하게되면 테스트가 오래걸리고, 특히 post요청은 실제 서버의 프로덕션 환경을 오염시킬 수 있다는 좋지 않은 방법이라고 설명이 되어있다.. 그래서 나의 테스트방법을 바꿔보기 위해 찾아보게 되었다.

1. URLSessionProtocol 을 만들기

protocol URLSessionProtocol {
    func data(
        for request: URLRequest, 
        delegate: URLSessionTaskDelegate?
    ) async throws -> (Data, URLResponse)
}

내가 사용하는 URLSession.shared.data(for:) 메서드와 같은 정의로 만들어 준다.

2. MockURLSession 만들기

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 이다.

3. URLSessionProtocol 을 기존 코드에 적용하기

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 둘다 들어올 수 있도록 만든다.

4. statusCode 테스트

 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 범위를 벗어날 때 오류가 정상적으로 출력됨을 보장받을 수 있다.

5. Decoding Test

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 을 만들어 테스트 하는 방법을 직접 사용해보니 어떤 응답이 주어졌을 때, 내가만든 네트워킹 객체가 응답을 적절하게 처리를 해주는가(특히 오류처리를 잘 하는가)에 대한 테스트를 가능케 한 것 같다.

🔗 Reference

profile
IOS 앱개발 공부

0개의 댓글