Testing using mocks
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 XCTest | |
@testable import WeatherApp | |
extension XCTestCase { // JSON support | |
func jsonData(payload: Any) throws -> Data { | |
let options: JSONSerialization.WritingOptions = [.prettyPrinted, .sortedKeys] | |
let data = try JSONSerialization.data(withJSONObject: payload, options: options) | |
return data | |
} | |
} | |
class WeatherAppTests: XCTestCase { | |
override func setUp() { | |
super.setUp() | |
// Put setup code here. This method is called before the invocation of each test method in the class. | |
} | |
override func tearDown() { | |
// Put teardown code here. This method is called after the invocation of each test method in the class. | |
super.tearDown() | |
} | |
func testOpenWeatherMap() { | |
let exp = expectation(description: "Get weather data") | |
let session = MockURLSession() | |
self.prepareRetreiveWeatherSessionDataSucess(session) | |
let controller = OpenWeatherMapNetworkController(session: session) | |
let input = Input(location: "Campana", unit: TemperatureUnit.metric) | |
controller.fetchCurrentWeatherData(input: input) { (result: Either<NetworkControllerError, WeatherData>) in | |
switch result { | |
case .left: | |
XCTFail("no data returned by fetchWeatherData()") | |
case .right(let data): | |
let city = input.location | |
let condition = data.condition | |
let temperature = data.temperature | |
XCTAssertEqual(city, "Campana") | |
XCTAssertEqual(condition, "Clear") | |
XCTAssertEqual(temperature, 42.8) | |
exp.fulfill() | |
} | |
} | |
waitForExpectations(timeout: 10, handler: nil) | |
} | |
func testOpenWeatherShouldFailWithError() { | |
let exp = expectation(description: "Get weather data") | |
let session = MockURLSession() | |
let error = NSError(domain: "asd", code: 123, userInfo: nil) | |
session.error = error as Error | |
self.prepareRetreiveWeatherSessionDataSucess(session) | |
let controller = OpenWeatherMapNetworkController(session: session) | |
let input = Input(location: "Campana", unit: TemperatureUnit.metric) | |
controller.fetchCurrentWeatherData(input: input) { (result: Either<NetworkControllerError, WeatherData>) in | |
switch result { | |
case .left(let error): | |
XCTAssertNotNil(error) | |
exp.fulfill() | |
case .right: | |
XCTFail("It should fail") | |
} | |
} | |
waitForExpectations(timeout: 10, handler: nil) | |
} | |
} | |
extension WeatherAppTests { // MARK: Session mock production | |
fileprivate func prepareRetreiveWeatherSessionDataSucess(_ session: MockURLSession) { | |
let dataPayload: [String: Any] = [ | |
"weather": [ | |
[ | |
"id": 800, | |
"main": "Clear", | |
"description": "clear sky", | |
"icon": "01n" | |
] | |
], | |
"main": | |
[ | |
"temp": 42.8, | |
"pressure": 1019, | |
"humidity": 100, | |
"temp_min": 42.8, | |
"temp_max": 42.8 | |
] | |
] | |
do { | |
let data = try self.jsonData(payload: dataPayload) | |
session.data = data | |
} catch { | |
print(error) | |
} | |
} | |
} | |
class MockURLSessionDataTask: URLSessionDataTask { | |
// I know, against Apple naming convention, | |
// but I rather have the `Mock` part right on front | |
// to prevent autocompletion to introduce errors. | |
private let closure: () -> Void | |
init(closure: @escaping () -> Void) { | |
self.closure = closure | |
} | |
override func resume() { | |
self.closure() | |
} | |
} | |
class MockURLSession: URLSession { | |
typealias CompletionHandler = (Data?, URLResponse?, Error?) -> Void | |
var data: Data? | |
var error: Error? | |
var response: URLResponse? | |
override func dataTask(with request: URLRequest, | |
completionHandler: @escaping CompletionHandler) -> URLSessionDataTask { | |
let data = self.data | |
let response = self.response | |
let error = self.error | |
let dataTask = MockURLSessionDataTask(closure: { | |
completionHandler(data, response, error) | |
}) | |
return dataTask | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment