Last active
May 28, 2020 21:48
-
-
Save jaredsinclair/926019df365cb33fbd40e9fdb903bf4b to your computer and use it in GitHub Desktop.
Exposing private members to tests via composition.
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
import Foundation | |
/// This class is public, because my library exposes this to the rest of my app. | |
public class ObligatoryNetworkingClient { | |
public enum GetEndpoint { | |
case timeline | |
case profile(userID: String) | |
case directMessages | |
} | |
/// Nobody has access to **the** request builder. | |
private var builder = URLRequestBuilder() | |
/// Nobody has access to **the** response parser. | |
private var parser = URLResponseParser() | |
/// I might never, ever bother with writing tests of this `get(...)` method | |
/// because it's just glue code. It doesn't contain any business logic, just | |
/// calls into single-purpose utility objects. Testing this method would also | |
/// require doing really, really gross things to the layout of this class in | |
/// order to prepare the object for the test, and I don't wanna do that. | |
public func get(_ endpoint: GetEndpoint, completion: @escaping (Result<Data, Error>) -> Void) { | |
func finish(with result: Result<Data, Error>) { | |
DispatchQueue.main.async { completion(result) } | |
} | |
let request: URLRequest | |
do { | |
request = try builder.buildRequest(for: endpoint) | |
} catch { | |
finish(with: .failure(error)) | |
return | |
} | |
URLSession.shared.dataTask(with: request) { (d, r, e) in | |
do { | |
let data = try self.parser.parse(response: r, data: d, error: e) | |
finish(with: .success(data)) | |
} catch { | |
finish(with: .failure(error)) | |
} | |
}.resume() | |
} | |
} | |
extension ObligatoryNetworkingClient { | |
/// This struct could be either `public` or `internal`, it doesn't matter. | |
/// Either way, unit tests can see it, and consumers of my library can't | |
/// interfere with the operation of `ObligatoryNetworkingClient` because | |
/// it's `builder` property is `private`. | |
internal struct URLRequestBuilder { | |
internal var accountID: String? | |
internal var authenticationToken: String? | |
internal var applicationSecret: String? | |
internal enum Error: Swift.Error { | |
case missingAuthenticationToken | |
case etcetera | |
} | |
/// Value in, value out. Unit test the living **shit** out of this method. | |
internal func buildRequest(for endpoint: GetEndpoint) throws -> URLRequest { | |
// Insert a shit ton of logic here. | |
throw URLRequestBuilder.Error.missingAuthenticationToken | |
} | |
} | |
} | |
extension ObligatoryNetworkingClient { | |
/// This struct could be either `public` or `internal`, it doesn't matter. | |
/// Either way, unit tests can see it, and consumers of my library can't | |
/// interfere with the operation of `ObligatoryNetworkingClient` because | |
/// it's `parser` property is `private`. | |
internal struct URLResponseParser { | |
internal enum Error: Swift.Error { | |
case requestTimedOut | |
case etcetera | |
} | |
/// Value in, value out. Unit test the living **shit** out of this method. | |
internal func parse(response: URLResponse?, data: Data?, error: Swift.Error?) throws -> Data { | |
// Insert a shit ton of logic here. | |
throw URLResponseParser.Error.requestTimedOut | |
} | |
} | |
} | |
// MEANWHILE, IN A UNIT TEST......... | |
let builder = URLRequestBuilder( | |
accountID: "abc123", | |
authenticationToken: nil, | |
applicationSecret: "shhhhh") | |
do { | |
_ = try builder.buildRequest(for: .timeline) | |
XCTFail("This should have failed.") | |
} catch ObligatoryNetworkingClient.URLRequestBuilder.Error.missingAuthenticationToken { | |
// Hooray! | |
} catch { | |
XCTFail("Wrong error: \(error)") | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment