앱이 네트워크 기능을 가지고 있다면, 이것을 테스트하는것은 중요합니다.
네트워킹하는 객체는 각기 다른 네트워크 응답에 의도한대로 기능하고 결과를 반환해야 합니다.
의도한대로 동작하지 않을 수 있다는 것을 알면서도
배포할 생각이신가요?
테스트는 만능이 아닙니다만, 적어도 임의의 테스트 입력에 대해서 의도한대로 동작 하는지 정도는 알 수 있습니다.
(네트워크에만 국한되는 이야기는 아니지만요!)
URLSession을 사용하는 네트워크 객체는 어떻게 테스트할 수 있을까?
iOS앱에서 네트워킹 기능을 제공하기 위해서는 여러 프레임워크를 사용할 수 있습니다.
그중 URLSession을 사용했을 경우를 가정하겠습니다.
기존에는 URLSession을 테스트하려면, DataTask등의 함수를 Warping하는 프로토콜을 선언했습니다.
그리고 이것을 URLSession과 Mock객체에게 채택하고, 구현체나 Mock객체를 주입받는 식으로 테스트를 했었죠.
// 출처 - 우아한 기술 블로그, https://techblog.woowahan.com/2704/
///가로챌 메소드를 정의한 프로토콜 선언
protocol URLSessionProtocol {
func dataTask(with request: URLRequest,
completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask
}
extension URLSession
//resume를 가로챌 목객체 구현
class MockURLSessionDataTask: URLSessionDataTask {
override init()
var resumeDidCall
override func resume() {
resumeDidCall()
}
}
//실제 네트워크를 수행하는것 처럼 동작하는 Mock객체 구현
class MockURLSession: URLSessionProtocol {
var makeRequestFail = false
init(makeRequestFail: Bool = false) {
self.makeRequestFail = makeRequestFail
}
var sessionDataTask: MockURLSessionDataTask?
// dataTask 를 구현합니다.
func dataTask(with request: URLRequest,
completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
// 성공시 callback 으로 넘겨줄 response
let successResponse = ...
// 실패시 callback 으로 넘겨줄 response
let failureResponse = ...
let sessionDataTask = MockURLSessionDataTask()
// resume() 이 호출되면 completionHandler() 가 호출되도록 합니다.
sessionDataTask.resumeDidCall = {
if self.makeRequestFail {
completionHandler(nil, failureResponse, nil)
} else {
completionHandler(JokesAPI.randomJokes.sampleData, successResponse, nil)
}
}
self.sessionDataTask = sessionDataTask
return sessionDataTask
}
}
// 테스트할 네트워킹 SUT에 Mock객체 주입 및 테스트
class JokesAPIProviderTests: XCTestCase {
var sut: JokesAPIProvider!
override func setUpWithError() throws {
sut = .init(session: MockURLSession())
}
func test_fetchRandomJoke() {
let expectation = XCTestExpectation()
let response = try? JSONDecoder().decode(JokeReponse.self,
from: JokesAPI.randomJokes.sampleData)
sut.fetchRandomJoke { result in
switch result {
case .success(let joke):
XCTAssertEqual(joke.id, response?.value.id)
XCTAssertEqual(joke.joke, response?.value.joke)
case .failure:
XCTFail()
}
expectation.fulfill()
}
wait(for: [expectation], timeout: 2.0)
}
//....
작업량이 많아 보입니다만, 테스트할 수 있다는 것에 감사할 일입니다.
그러나 Async/Await, Combine
이 등장하고, URLSession의 메서드도 다양해졌습니다.
더이상 dataTask() 메소드만이 아닌, data(), DataTaskPublisher()
에 대응하는 코드도 작성해주어야 합니다.
보일러 플레이트 코드
가 발생할것이 눈에 뻔히 보이는 상황.
swift에서는 하나의 목객체만으로 네트워킹을 테스트 하기 어려운걸까요?
URLProtocol은 무엇이 다를까?
결론부터 적자면, client 프로퍼티를 사용해 URLSession의 내부 동작 자체를 재정의한다는 점이 다릅니다.

"Behind the scenes though, there's another lower-level API URLProtocol which performs the underlying work of opening network connection, writing the request, and reading back a response.
URLProtocol is designed to be subclassed giving an extensibility point for the URL loading system.""URLProtocol은 URL loading system의 확장 가능성에 대해 상속되도록 설계됐습니다"
- WWDC2018, Testing Tips and Tricks
WWDC의 내용에 비추어 생각해보면, dataTask(), data(), DataTaskPublisher()
등의 메소드들은 lower-level API
인 URLProtocol
의 API를 사용하여 네트워킹 작업을 수행할 것 입니다.
그래서 URLProtocol
을 가로채면, 이 API를 사용하는 상위 API들의 입출력을 가로챌 수 있는 것
이죠.
URLProtocol를 간단히 알아보자
URLProtocol
class URLProtocol : NSObject
An abstract class that handles the loading of protocol-specific URL data.
URLProtocol은 추상클래스입니다. (?? Swift 너 그런거 없다며)
따라서 직접 생성하지 말고, 상속받고 메서드들을 override해 사용해야 합니다.
// WWDC2018에서 보여준 예시에 가까운 코드
final class MockURLProtocol: URLProtocol {
//사용자 정의 프로퍼티입니다.
//request에 대한 동작을 정의할 인터페이스는 핸들러, 배열 등 무엇도 상관 없습니다.
static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))?
//반드시 override해야합니다.
override class func canInit(with request: URLRequest) -> Bool {
//false면 커스텀한 응답 방식을 사용하지 않습니다.
return true
}
//반드시 override해야합니다.
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
//URLSession의 request를 조작할 수 있습니다.
return request
}
//반드시 override해야합니다.
override func startLoading() {
guard let handler = MockURLProtocol.requestHandler else {
XCTFail("Received unexpected request with no handler set")
return
}
do {
//반환할 response, data를 바꿔치기 할 수 있습니다.
let (response, data) = try handler(request)
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
client?.urlProtocol(self, didLoad: data)
client?.urlProtocolDidFinishLoading(self)
} catch {
client?.urlProtocol(self, didFailWithError: error)
}
}
//반드시 override해야합니다.
override func stopLoading() {
}
}
client 프로퍼티
는 URLProtocol에서 URL loading system과 상호작용하기 위해 제공되는 인터페이스입니다.
테스트를 위해 URL Loading system이 할 일을 직접 정의하고 있는 것 이므로, URL loading system의 상태를 직접 조작해주어야 합니다.
거창하지만, 간단히 말해 URLSession이 반환할 정보를 client 프로퍼티의 메소드
로 조작할 수 있다는 말 입니다.
//반환할 응답을 주입하는 메서드
func urlProtocol(URLProtocol, didReceive: URLResponse, cacheStoragePolicy: URLCache.StoragePolicy)
//반환할 에러를 주입하는 메서드
func urlProtocol(URLProtocol, didFailWithError: any Error)
//반환할 데이터를 주입하는 메서드
func urlProtocol(URLProtocol, didLoad: Data)
//네트워킹이 끝났음을 알리는 메서드.
//호출하지 않으면 네트워킹을 지속하는것으로 간주합니다.
func urlProtocolDidFinishLoading(URLProtocol)
이를 다음과 같이 테스트에 활용할 수 있습니다.
final class ProviderTest: XCTestCase {
/// URLSession을 사용하는 네트워킹 객체
var provider: Provider!
/// Mock객체를 주입
override func setUpWithError() throws {
let configuration = URLSessionConfiguration.ephemeral
configuration.protocolClasses = [MockURLProtocol.self]
let urlSession = URLSession(configuration: configuration)
provider = ProviderImpl(session: urlSession)
}
func test_provider_returns_HTTPStatusCodeError404() async throws {
//Mock객체의 응답과 데이터를 조작
MockURLProtocol.requestHandler = { (request:URLRequest) throws in
let failureResponse: HTTPURLResponse = HTTPURLResponse(url: request.url!,
statusCode: 404,
httpVersion: nil,
headerFields: nil)!
let mockJSONData = "{\"statusCode\":\"404\"}".data(using: .utf8)!
return (failureResponse, mockJSONData)
}
let request = StubEndpint()
do {
_ = try await provider.request(with: request)
} catch {
XCTAssertEqual(error, HTTPStatusCodeError.clientError(code: 404))
return
}
XCTFail("에러를 반환하지 않음")
}
한 걸음 더
URLProtocol이 반환할 응답과 데이터를 주입하기 위한 인터페이스는 클로저(핸들러)가 아니어도 괜찮습니다.
사용자가 테스트하기 용이하도록 인터페이스를 커스텀할 수도 있습니다.
아래는 딕셔너리를 인터페이스로, 여러 URL에 대해 각 URL에 따른 응답, 데이터, 에러를 주입하고 사용하는 방법에 대한 예시입니다.
/// https://gist.github.com/soujohnreis/7c86965efbbb2297d4db3f84027327c1
class URLProtocolMock: URLProtocol {
/// Dictionary maps URLs to tuples of error, data, and response
static var mockURLs = [URL?: (error: Error?, data: Data?, response: HTTPURLResponse?)]()
override class func canInit(with request: URLRequest) -> Bool {
// Handle all types of requests
return true
}
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
// Required to be implemented here. Just return what is passed
return request
}
override func startLoading() {
if let url = request.url {
if let (error, data, response) = URLProtocolMock.mockURLs[url] {
// We have a mock response specified so return it.
if let responseStrong = response {
self.client?.urlProtocol(self, didReceive: responseStrong, cacheStoragePolicy: .notAllowed)
}
// We have mocked data specified so return it.
if let dataStrong = data {
self.client?.urlProtocol(self, didLoad: dataStrong)
}
// We have a mocked error so return it.
if let errorStrong = error {
self.client?.urlProtocol(self, didFailWithError: errorStrong)
}
}
}
// Send the signal that we are done returning our mock response
self.client?.urlProtocolDidFinishLoading(self)
}
override func stopLoading() {
// Required to be implemented. Do nothing here.
}
}
// usage
let response = HTTPURLResponse(url: urlRequestedMock1, statusCode: 200, httpVersion: nil, headerFields: nil)
let error: Error? = nil
let data = """
[
{
"someJsonKey": "someJsonData",
"anotherJsonKey": "anotherJsonData"
}
]
""".data(using: .utf8)
//이곳에서 URL과 응답, 데이터, 에러를 주입
URLProtocolMock.mockURLs = [
urlRequestedMock1: (error, data, response),
//...
]
let sessionConfiguration = URLSessionConfiguration.ephemeral
sessionConfiguration.protocolClasses = [URLProtocolMock.self]
let mockedSession = URLSession(configuration: sessionConfiguration)
let myAwesomeNetworkService = MyAwesomeNetworkService(urlSession: mockedSession)
myAwesomeNetworkService.fetchDataFromAwesomeUrl { response in
// your awesome assertions
}
참고자료
https://developer.apple.com/documentation/foundation/urlprotocol
URLProtocol | Apple Developer Documentation
An abstract class that handles the loading of protocol-specific URL data.
developer.apple.com
https://developer.apple.com/documentation/foundation/url_loading_system
URL Loading System | Apple Developer Documentation
Interact with URLs and communicate with servers using standard Internet protocols.
developer.apple.com
https://developer.apple.com/videos/play/wwdc2018/417/
Testing Tips & Tricks - WWDC18 - Videos - Apple Developer
Testing is an essential tool to consistently verify your code works correctly, but often your code has dependencies that are out of your...
developer.apple.com
https://techblog.woowahan.com/2704/
iOS Networking and Testing | 우아한형제들 기술블로그
Why Networking? Networking은 요즘 앱에서 거의 필수적인 요소입니다. 설치되어 있는 앱들 중에 네트워킹을 사용하지 않는 앱은 거의 없을 겁니다. API 추가가 쉽고 변경이 용이한 네트워킹 모듈을 개발
techblog.woowahan.com
https://gist.github.com/soujohnreis/7c86965efbbb2297d4db3f84027327c1
Mock URLSession using URLProtocol
Mock URLSession using URLProtocol. GitHub Gist: instantly share code, notes, and snippets.
gist.github.com
'learnings > Swift' 카테고리의 다른 글
[iOS/Swift] 뷰 좌표계와 UIView.Frame (2) | 2024.08.26 |
---|---|
[iOS/Swift] 소멸자를 사용한 메모리 해제 (0) | 2024.03.26 |
[iOS/Swift] 인스턴스의 순환 참조에 의한 메모리 누수 (0) | 2024.03.21 |
[iOS/Swift] TableView에서 SnapScroll (magnetic scroll) 구현 (0) | 2023.01.20 |
[iOS/Swift] #1-2 나의 첫 토이프로젝트 (0) | 2021.09.23 |
앱이 네트워크 기능을 가지고 있다면, 이것을 테스트하는것은 중요합니다.
네트워킹하는 객체는 각기 다른 네트워크 응답에 의도한대로 기능하고 결과를 반환해야 합니다.
의도한대로 동작하지 않을 수 있다는 것을 알면서도
배포할 생각이신가요?
테스트는 만능이 아닙니다만, 적어도 임의의 테스트 입력에 대해서 의도한대로 동작 하는지 정도는 알 수 있습니다.
(네트워크에만 국한되는 이야기는 아니지만요!)
URLSession을 사용하는 네트워크 객체는 어떻게 테스트할 수 있을까?
iOS앱에서 네트워킹 기능을 제공하기 위해서는 여러 프레임워크를 사용할 수 있습니다.
그중 URLSession을 사용했을 경우를 가정하겠습니다.
기존에는 URLSession을 테스트하려면, DataTask등의 함수를 Warping하는 프로토콜을 선언했습니다.
그리고 이것을 URLSession과 Mock객체에게 채택하고, 구현체나 Mock객체를 주입받는 식으로 테스트를 했었죠.
// 출처 - 우아한 기술 블로그, https://techblog.woowahan.com/2704/
///가로챌 메소드를 정의한 프로토콜 선언
protocol URLSessionProtocol {
func dataTask(with request: URLRequest,
completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask
}
extension URLSession
//resume를 가로챌 목객체 구현
class MockURLSessionDataTask: URLSessionDataTask {
override init()
var resumeDidCall
override func resume() {
resumeDidCall()
}
}
//실제 네트워크를 수행하는것 처럼 동작하는 Mock객체 구현
class MockURLSession: URLSessionProtocol {
var makeRequestFail = false
init(makeRequestFail: Bool = false) {
self.makeRequestFail = makeRequestFail
}
var sessionDataTask: MockURLSessionDataTask?
// dataTask 를 구현합니다.
func dataTask(with request: URLRequest,
completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
// 성공시 callback 으로 넘겨줄 response
let successResponse = ...
// 실패시 callback 으로 넘겨줄 response
let failureResponse = ...
let sessionDataTask = MockURLSessionDataTask()
// resume() 이 호출되면 completionHandler() 가 호출되도록 합니다.
sessionDataTask.resumeDidCall = {
if self.makeRequestFail {
completionHandler(nil, failureResponse, nil)
} else {
completionHandler(JokesAPI.randomJokes.sampleData, successResponse, nil)
}
}
self.sessionDataTask = sessionDataTask
return sessionDataTask
}
}
// 테스트할 네트워킹 SUT에 Mock객체 주입 및 테스트
class JokesAPIProviderTests: XCTestCase {
var sut: JokesAPIProvider!
override func setUpWithError() throws {
sut = .init(session: MockURLSession())
}
func test_fetchRandomJoke() {
let expectation = XCTestExpectation()
let response = try? JSONDecoder().decode(JokeReponse.self,
from: JokesAPI.randomJokes.sampleData)
sut.fetchRandomJoke { result in
switch result {
case .success(let joke):
XCTAssertEqual(joke.id, response?.value.id)
XCTAssertEqual(joke.joke, response?.value.joke)
case .failure:
XCTFail()
}
expectation.fulfill()
}
wait(for: [expectation], timeout: 2.0)
}
//....
작업량이 많아 보입니다만, 테스트할 수 있다는 것에 감사할 일입니다.
그러나 Async/Await, Combine
이 등장하고, URLSession의 메서드도 다양해졌습니다.
더이상 dataTask() 메소드만이 아닌, data(), DataTaskPublisher()
에 대응하는 코드도 작성해주어야 합니다.
보일러 플레이트 코드
가 발생할것이 눈에 뻔히 보이는 상황.
swift에서는 하나의 목객체만으로 네트워킹을 테스트 하기 어려운걸까요?
URLProtocol은 무엇이 다를까?
결론부터 적자면, client 프로퍼티를 사용해 URLSession의 내부 동작 자체를 재정의한다는 점이 다릅니다.

"Behind the scenes though, there's another lower-level API URLProtocol which performs the underlying work of opening network connection, writing the request, and reading back a response.
URLProtocol is designed to be subclassed giving an extensibility point for the URL loading system.""URLProtocol은 URL loading system의 확장 가능성에 대해 상속되도록 설계됐습니다"
- WWDC2018, Testing Tips and Tricks
WWDC의 내용에 비추어 생각해보면, dataTask(), data(), DataTaskPublisher()
등의 메소드들은 lower-level API
인 URLProtocol
의 API를 사용하여 네트워킹 작업을 수행할 것 입니다.
그래서 URLProtocol
을 가로채면, 이 API를 사용하는 상위 API들의 입출력을 가로챌 수 있는 것
이죠.
URLProtocol를 간단히 알아보자
URLProtocol
class URLProtocol : NSObject
An abstract class that handles the loading of protocol-specific URL data.
URLProtocol은 추상클래스입니다. (?? Swift 너 그런거 없다며)
따라서 직접 생성하지 말고, 상속받고 메서드들을 override해 사용해야 합니다.
// WWDC2018에서 보여준 예시에 가까운 코드
final class MockURLProtocol: URLProtocol {
//사용자 정의 프로퍼티입니다.
//request에 대한 동작을 정의할 인터페이스는 핸들러, 배열 등 무엇도 상관 없습니다.
static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))?
//반드시 override해야합니다.
override class func canInit(with request: URLRequest) -> Bool {
//false면 커스텀한 응답 방식을 사용하지 않습니다.
return true
}
//반드시 override해야합니다.
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
//URLSession의 request를 조작할 수 있습니다.
return request
}
//반드시 override해야합니다.
override func startLoading() {
guard let handler = MockURLProtocol.requestHandler else {
XCTFail("Received unexpected request with no handler set")
return
}
do {
//반환할 response, data를 바꿔치기 할 수 있습니다.
let (response, data) = try handler(request)
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
client?.urlProtocol(self, didLoad: data)
client?.urlProtocolDidFinishLoading(self)
} catch {
client?.urlProtocol(self, didFailWithError: error)
}
}
//반드시 override해야합니다.
override func stopLoading() {
}
}
client 프로퍼티
는 URLProtocol에서 URL loading system과 상호작용하기 위해 제공되는 인터페이스입니다.
테스트를 위해 URL Loading system이 할 일을 직접 정의하고 있는 것 이므로, URL loading system의 상태를 직접 조작해주어야 합니다.
거창하지만, 간단히 말해 URLSession이 반환할 정보를 client 프로퍼티의 메소드
로 조작할 수 있다는 말 입니다.
//반환할 응답을 주입하는 메서드
func urlProtocol(URLProtocol, didReceive: URLResponse, cacheStoragePolicy: URLCache.StoragePolicy)
//반환할 에러를 주입하는 메서드
func urlProtocol(URLProtocol, didFailWithError: any Error)
//반환할 데이터를 주입하는 메서드
func urlProtocol(URLProtocol, didLoad: Data)
//네트워킹이 끝났음을 알리는 메서드.
//호출하지 않으면 네트워킹을 지속하는것으로 간주합니다.
func urlProtocolDidFinishLoading(URLProtocol)
이를 다음과 같이 테스트에 활용할 수 있습니다.
final class ProviderTest: XCTestCase {
/// URLSession을 사용하는 네트워킹 객체
var provider: Provider!
/// Mock객체를 주입
override func setUpWithError() throws {
let configuration = URLSessionConfiguration.ephemeral
configuration.protocolClasses = [MockURLProtocol.self]
let urlSession = URLSession(configuration: configuration)
provider = ProviderImpl(session: urlSession)
}
func test_provider_returns_HTTPStatusCodeError404() async throws {
//Mock객체의 응답과 데이터를 조작
MockURLProtocol.requestHandler = { (request:URLRequest) throws in
let failureResponse: HTTPURLResponse = HTTPURLResponse(url: request.url!,
statusCode: 404,
httpVersion: nil,
headerFields: nil)!
let mockJSONData = "{\"statusCode\":\"404\"}".data(using: .utf8)!
return (failureResponse, mockJSONData)
}
let request = StubEndpint()
do {
_ = try await provider.request(with: request)
} catch {
XCTAssertEqual(error, HTTPStatusCodeError.clientError(code: 404))
return
}
XCTFail("에러를 반환하지 않음")
}
한 걸음 더
URLProtocol이 반환할 응답과 데이터를 주입하기 위한 인터페이스는 클로저(핸들러)가 아니어도 괜찮습니다.
사용자가 테스트하기 용이하도록 인터페이스를 커스텀할 수도 있습니다.
아래는 딕셔너리를 인터페이스로, 여러 URL에 대해 각 URL에 따른 응답, 데이터, 에러를 주입하고 사용하는 방법에 대한 예시입니다.
/// https://gist.github.com/soujohnreis/7c86965efbbb2297d4db3f84027327c1
class URLProtocolMock: URLProtocol {
/// Dictionary maps URLs to tuples of error, data, and response
static var mockURLs = [URL?: (error: Error?, data: Data?, response: HTTPURLResponse?)]()
override class func canInit(with request: URLRequest) -> Bool {
// Handle all types of requests
return true
}
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
// Required to be implemented here. Just return what is passed
return request
}
override func startLoading() {
if let url = request.url {
if let (error, data, response) = URLProtocolMock.mockURLs[url] {
// We have a mock response specified so return it.
if let responseStrong = response {
self.client?.urlProtocol(self, didReceive: responseStrong, cacheStoragePolicy: .notAllowed)
}
// We have mocked data specified so return it.
if let dataStrong = data {
self.client?.urlProtocol(self, didLoad: dataStrong)
}
// We have a mocked error so return it.
if let errorStrong = error {
self.client?.urlProtocol(self, didFailWithError: errorStrong)
}
}
}
// Send the signal that we are done returning our mock response
self.client?.urlProtocolDidFinishLoading(self)
}
override func stopLoading() {
// Required to be implemented. Do nothing here.
}
}
// usage
let response = HTTPURLResponse(url: urlRequestedMock1, statusCode: 200, httpVersion: nil, headerFields: nil)
let error: Error? = nil
let data = """
[
{
"someJsonKey": "someJsonData",
"anotherJsonKey": "anotherJsonData"
}
]
""".data(using: .utf8)
//이곳에서 URL과 응답, 데이터, 에러를 주입
URLProtocolMock.mockURLs = [
urlRequestedMock1: (error, data, response),
//...
]
let sessionConfiguration = URLSessionConfiguration.ephemeral
sessionConfiguration.protocolClasses = [URLProtocolMock.self]
let mockedSession = URLSession(configuration: sessionConfiguration)
let myAwesomeNetworkService = MyAwesomeNetworkService(urlSession: mockedSession)
myAwesomeNetworkService.fetchDataFromAwesomeUrl { response in
// your awesome assertions
}
참고자료
https://developer.apple.com/documentation/foundation/urlprotocol
URLProtocol | Apple Developer Documentation
An abstract class that handles the loading of protocol-specific URL data.
developer.apple.com
https://developer.apple.com/documentation/foundation/url_loading_system
URL Loading System | Apple Developer Documentation
Interact with URLs and communicate with servers using standard Internet protocols.
developer.apple.com
https://developer.apple.com/videos/play/wwdc2018/417/
Testing Tips & Tricks - WWDC18 - Videos - Apple Developer
Testing is an essential tool to consistently verify your code works correctly, but often your code has dependencies that are out of your...
developer.apple.com
https://techblog.woowahan.com/2704/
iOS Networking and Testing | 우아한형제들 기술블로그
Why Networking? Networking은 요즘 앱에서 거의 필수적인 요소입니다. 설치되어 있는 앱들 중에 네트워킹을 사용하지 않는 앱은 거의 없을 겁니다. API 추가가 쉽고 변경이 용이한 네트워킹 모듈을 개발
techblog.woowahan.com
https://gist.github.com/soujohnreis/7c86965efbbb2297d4db3f84027327c1
Mock URLSession using URLProtocol
Mock URLSession using URLProtocol. GitHub Gist: instantly share code, notes, and snippets.
gist.github.com
'learnings > Swift' 카테고리의 다른 글
[iOS/Swift] 뷰 좌표계와 UIView.Frame (2) | 2024.08.26 |
---|---|
[iOS/Swift] 소멸자를 사용한 메모리 해제 (0) | 2024.03.26 |
[iOS/Swift] 인스턴스의 순환 참조에 의한 메모리 누수 (0) | 2024.03.21 |
[iOS/Swift] TableView에서 SnapScroll (magnetic scroll) 구현 (0) | 2023.01.20 |
[iOS/Swift] #1-2 나의 첫 토이프로젝트 (0) | 2021.09.23 |