Last active
March 22, 2021 20:24
-
-
Save luizmb/69a8cbc65a65e032241ad6fe3c0fceb7 to your computer and use it in GitHub Desktop.
Three Shades of Mock
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
let urlRequest = URLRequest(url: URL(string: "https://my.api.com")!) | |
// Option 1: Typealias to closure | |
typealias URLRequester1 = (URLRequest) -> AnyPublisher<(data: Data, response: URLResponse), URLError> | |
enum URLRequesterNamespace { | |
static var prod: URLRequester1 = { req in URLSession.shared.dataTaskPublisher(for: req).eraseToAnyPublisher() } | |
static func mock(alwaysReturns: (data: Data, response: URLResponse)) -> URLRequester1 { | |
return { _ in | |
Just(alwaysReturns).setFailureType(to: URLError.self).eraseToAnyPublisher() | |
} | |
} | |
static func mock(closure: @escaping (URLRequest) -> (data: Data, response: URLResponse)) -> URLRequester1 { | |
return { req in | |
Just(closure(req)).setFailureType(to: URLError.self).eraseToAnyPublisher() | |
} | |
} | |
} | |
func apiCall1(requester: @escaping URLRequester1) { | |
let cancellable = requester(urlRequest).sink(receiveCompletion: { _ in }, receiveValue: { _ in }) | |
} | |
func test1() { | |
apiCall1(requester: URLRequesterNamespace.prod) | |
apiCall1(requester: URLRequesterNamespace.mock(alwaysReturns: (data: Data(), response: HTTPURLResponse()))) | |
apiCall1(requester: URLRequesterNamespace.mock(closure: { _ in (data: Data(), response: HTTPURLResponse()) })) | |
} | |
// Option 2: Closure wrapper | |
struct URLRequester2 { | |
private let requester: (URLRequest) -> AnyPublisher<(data: Data, response: URLResponse), URLError> | |
init(_ requester: @escaping (URLRequest) -> AnyPublisher<(data: Data, response: URLResponse), URLError>) { | |
self.requester = requester | |
} | |
func callAsFunction(request: URLRequest) -> AnyPublisher<(data: Data, response: URLResponse), URLError> { | |
requester(request) | |
} | |
} | |
extension URLRequester2 { | |
static var prod: URLRequester2 = URLRequester2 { req in URLSession.shared.dataTaskPublisher(for: req).eraseToAnyPublisher() } | |
static func mock(alwaysReturns: (data: Data, response: URLResponse)) -> URLRequester2 { | |
URLRequester2 { _ in | |
Just(alwaysReturns).setFailureType(to: URLError.self).eraseToAnyPublisher() | |
} | |
} | |
static func mock(closure: @escaping (URLRequest) -> (data: Data, response: URLResponse)) -> URLRequester2 { | |
URLRequester2 { req in | |
Just(closure(req)).setFailureType(to: URLError.self).eraseToAnyPublisher() | |
} | |
} | |
} | |
func apiCall2(requester: URLRequester2) { | |
let cancellable = requester(request: urlRequest).sink(receiveCompletion: { _ in }, receiveValue: { _ in }) | |
} | |
func test2() { | |
apiCall2(requester: .prod) | |
apiCall2(requester: .mock(alwaysReturns: (data: Data(), response: HTTPURLResponse()))) | |
apiCall2(requester: .mock(closure: { _ in (data: Data(), response: HTTPURLResponse()) })) | |
} | |
// Option 3: Protocol (OOP) | |
protocol URLRequester3 { | |
func request(_ req: URLRequest) -> AnyPublisher<(data: Data, response: URLResponse), URLError> | |
} | |
struct URLRequester3Impl: URLRequester3 { | |
func request(_ req: URLRequest) -> AnyPublisher<(data: Data, response: URLResponse), URLError> { | |
URLSession.shared.dataTaskPublisher(for: req).eraseToAnyPublisher() | |
} | |
} | |
struct URLRequester3Mock: URLRequester3 { | |
private let requester: (URLRequest) -> AnyPublisher<(data: Data, response: URLResponse), URLError> | |
init(alwaysReturns: (data: Data, response: URLResponse)) { | |
requester = { _ in | |
Just(alwaysReturns).setFailureType(to: URLError.self).eraseToAnyPublisher() | |
} | |
} | |
init(closure: @escaping (URLRequest) -> (data: Data, response: URLResponse)) { | |
requester = { req in | |
Just(closure(req)).setFailureType(to: URLError.self).eraseToAnyPublisher() | |
} | |
} | |
func request(_ req: URLRequest) -> AnyPublisher<(data: Data, response: URLResponse), URLError> { | |
requester(req) | |
} | |
} | |
func apiCall3(requester: URLRequester3) { | |
let cancellable = requester.request(urlRequest).sink(receiveCompletion: { _ in }, receiveValue: { _ in }) | |
} | |
func test3() { | |
apiCall3(requester: URLRequester3Impl()) | |
apiCall3(requester: URLRequester3Mock(alwaysReturns: (data: Data(), response: HTTPURLResponse()))) | |
apiCall3(requester: URLRequester3Mock(closure: { _ in (data: Data(), response: HTTPURLResponse()) })) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Options 1 and 2 are equivalent, with a more Functional Programming style (you inject the closure). The Option 3 uses an OOP approach, by injecting instance that implements certain protocol.
All of them have pros and cons and there's no right or wrong. However, protocols in Swift are less flexible and once generics or associated types are needed, things become much more complex. On the other hand, the FP approach can be harder to learn, but once learned, it offers more flexibility.
The one to choose will depend on the skills and characteristics of your team.