我們把請(qǐng)求DarkSky的代碼封裝起來(lái),以降低這部分代碼在未來(lái)對(duì)我們App的影響。并為這部分的單元測(cè)試,做一些準(zhǔn)備工作。
設(shè)計(jì)DataManager
為了封裝DarkSky的請(qǐng)求,我們?cè)赟ky中新建一個(gè)分組:Manager,并在其中添加一個(gè)WeatherDataManager.swif文件。在這里,我們創(chuàng)建一個(gè)class WeatherDataManager來(lái)管理對(duì)DarkSky的請(qǐng)求:
final class WeatherDataManager { }
這里,由于WeatherDataManager不會(huì)作為其它類的基類,我們?cè)诼暶髦惺褂昧?code>final關(guān)鍵字,可以提高這個(gè)對(duì)象的訪問(wèn)性能。
WeatherDataManager有一個(gè)屬性,表示請(qǐng)求的URL:
final class WeatherDataManager {
private let baseURL: URL
}
然后,我們用下面的代碼創(chuàng)建一個(gè)單例,便于我們用一致的方式請(qǐng)求天氣數(shù)據(jù):
final class WeatherDataManager {
private let baseURL: URL
private init(baseURL: URL) {
self.baseURL = baseURL
}
static let shared =
WeatherDataManager(API.authenticatedUrl)
}
這樣,我們就只能通過(guò)WeatherDataManager.shared這樣的形式,來(lái)訪問(wèn)WeatherDataManager對(duì)象了。
接下來(lái),我們要在WeatherDataManager中創(chuàng)建一個(gè)根據(jù)地理位置返回天氣信息的方法。由于網(wǎng)絡(luò)請(qǐng)求是異步的,這個(gè)過(guò)程只能通過(guò)回調(diào)函數(shù)完成。因此,這個(gè)方法看上去應(yīng)該是這樣的:
final class WeatherDataManager {
// ...
typealias CompletionHandler =
(WeatherData?, DataManagerError?) -> Void
func weatherDataAt(
latitude: Double,
longitude: Double,
completion: @escaping CompletionHandler) {}
}
然后,我們來(lái)定義獲取數(shù)據(jù)時(shí)的錯(cuò)誤:
enum DataManagerError: Error {
case failedRequest
case invalidResponse
case unknown
}
簡(jiǎn)單起見(jiàn),我們只定義了三種情況:非法請(qǐng)求、非法返回以及未知錯(cuò)誤。然后,我們來(lái)實(shí)現(xiàn)weatherAt方法,它的邏輯很簡(jiǎn)單,只是按約定拼接URL,設(shè)置HTTP header,然后使用URLSession發(fā)起請(qǐng)求就好了:
func weatherDataAt(latitude: Double,
longitude: Double,
completion: @escaping CompletionHandler) {
// 1\. Concatenate the URL
let url = baseURL.appendingPathComponent("\(latitude), \(longitude)")
var request = URLRequest(url: url)
// 2\. Set HTTP header
request.setValue("application/json",
forHTTPHeaderField: "Content-Type")
request.httpMethod = "GET"
// 3\. Launch the request
URLSession.shared.dataTask(
with: request, completionHandler: {
(data, response, error) in
// 4\. Get the response here
}).resume()
}
在dataTask的completionHandler中,為了讓代碼看上去干凈一些,我們只調(diào)用一個(gè)幫助函數(shù):
URLSession.shared.dataTask(with: request,
completionHandler: {
(data, response, error) in
DispatchQueue.main.async {
self.didFinishGettingWeatherData(
data: data,
response: response,
error: error,
completion: completion)
}
}).resume()
這里,為了保證可以在dataTask的回調(diào)函數(shù)中更新UI,我們把它派發(fā)到主線程隊(duì)列執(zhí)行。完成后,我們來(lái)實(shí)現(xiàn)didFinishGettingWeatherData:
func didFinishGettingWeatherData(
data: Data?,
response: URLResponse?,
error: Error?,
completion: CompletionHandler) {
if let _ = error {
completion(nil, .failedRequest)
}
else if let data = data,
let response = response as? HTTPURLResponse {
if response.statusCode == 200 {
do {
let weatherData =
try JSONDecoder().decode(WeatherData.self, from: data)
completion(weatherData, nil)
}
catch {
completion(nil, .invalidResponse)
}
}
else {
completion(nil, .failedRequest)
}
}
else {
completion(nil, .unknown)
}
}
其實(shí)邏輯很簡(jiǎn)單,就是根據(jù)請(qǐng)求以及服務(wù)器的返回值是否可用,把對(duì)應(yīng)的參數(shù)傳遞給了一個(gè)可以自定義的回調(diào)函數(shù)。這樣,這個(gè)WeatherDataManager就實(shí)現(xiàn)好了。
現(xiàn)在,回想起來(lái),我們?cè)谶@兩節(jié)中,關(guān)于model的部分,已經(jīng)寫了不少的代碼了,它們真的能正常工作么?我們?nèi)绾未_定這個(gè)事情呢?在把model關(guān)聯(lián)到controller之前,我們最好確定一下。
當(dāng)然,一個(gè)直觀的辦法就是在類似某個(gè)viewDidLoad之類的方法里,寫個(gè)代碼實(shí)際請(qǐng)求一下看看。但是估計(jì)你也能感覺(jué)到這種做法并不地道,如果未來(lái)你修改了Manager的代碼呢?難道還要重新找個(gè)viewDidLoad方法插個(gè)空來(lái)測(cè)試么?估計(jì)你自己都不太敢這樣做,萬(wàn)一你在恢復(fù)的時(shí)候不慎修改掉了哪部分功能代碼,就很容易隨隨便便坑上你幾個(gè)小時(shí)。
為此,我們需要一種更專業(yè)和安全的方式,來(lái)確定局部代碼的正確性。這種方式,就是單元測(cè)試。在開(kāi)始測(cè)試我們的WeatherDataManager之前,我們要先了解一下Xcode提供的單元測(cè)試模板。
了解單元測(cè)試模板
首先,在Xcode默認(rèn)創(chuàng)建的SkyTests分組中,刪掉默認(rèn)的SkyTests.swift。然后在SkyTests Group上點(diǎn)右鍵,選擇New File...:

其次,在右上角的filter中,輸入unit,找到單元測(cè)試的模板。選中Unit Test Case Class,點(diǎn)擊Next:

第三,給測(cè)試用例起個(gè)名字,例如WeatherDataManagerTest。這個(gè)名字最好可以直接表達(dá)我們要測(cè)試的內(nèi)容。這樣,不同的開(kāi)發(fā)者都可以方便的了解到實(shí)際測(cè)試的內(nèi)容:

第四,接下來(lái),Xcode就會(huì)提示我們是否需要?jiǎng)?chuàng)建一個(gè)bridge header,由于我們?cè)诩僑wift環(huán)境中開(kāi)發(fā),因此,選擇Don't Create,并點(diǎn)擊Finish按鈕。
設(shè)置好保存路徑之后,我們就可以在SkyTests分組中,找到新添加的測(cè)試用例了。在開(kāi)始編寫測(cè)試之前,這個(gè)文件中有幾個(gè)值得說(shuō)明的地方:
首先,在文件一開(kāi)始,要添加下面的代碼引入項(xiàng)目的main module。這樣,才能在測(cè)試用例中,訪問(wèn)到項(xiàng)目定義的類型:
import XCTest
@testable import Sky
其次,在生成的代碼中,WeatherDataManagerTest派生自XCTestCase,表示這是一個(gè)測(cè)試用例。
第三,在WeatherDataManagerTest里,我們可以把所有的測(cè)試前要準(zhǔn)備的代碼,寫在setUp方法里,而把測(cè)試后需要清理的代碼,寫在tearDown方法里。這里要注意下面代碼中注釋的位置,初始化代碼寫在super.setUp()后面,清理代碼要寫在super.tearDown()前面:
class WeatherDataManagerTest: XCTestCase {
override func setUp() {
super.setUp()
// Your set up code here...
}
override func tearDown() {
// Your tear down code here...
super.tearDown()
}
// ...
}
第四,Xcode為我們生成了兩個(gè)默認(rèn)的測(cè)試方法:
class WeatherDataManagerTest: XCTestCase {
func testExample() {
// ...
}
func testPerformanceExample() {
// ...
}
}
要注意的是,所有測(cè)試方法都必須用test開(kāi)頭,Xcode才會(huì)識(shí)別它們并自動(dòng)執(zhí)行。這里,可以先把它們刪掉,稍后我們會(huì)編寫自己的測(cè)試方法。