Skip to content

Instantly share code, notes, and snippets.

@luizmb
Last active March 22, 2021 20:24
Show Gist options
  • Save luizmb/69a8cbc65a65e032241ad6fe3c0fceb7 to your computer and use it in GitHub Desktop.
Save luizmb/69a8cbc65a65e032241ad6fe3c0fceb7 to your computer and use it in GitHub Desktop.
Three Shades of Mock
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()) }))
}
@luizmb
Copy link
Author

luizmb commented Mar 22, 2021

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment