我們把請(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)寫(xiě)了不少的代碼了概而,它們真的能正常工作么?我們?nèi)绾未_定這個(gè)事情呢囱修?在把model關(guān)聯(lián)到controller之前赎瑰,我們最好確定一下。
當(dāng)然破镰,一個(gè)直觀的辦法就是在類似某個(gè)viewDidLoad
之類的方法里餐曼,寫(xiě)個(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)始編寫(xiě)測(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)備的代碼咖杂,寫(xiě)在setUp
方法里庆寺,而把測(cè)試后需要清理的代碼,寫(xiě)在tearDown
方法里诉字。這里要注意下面代碼中注釋的位置懦尝,初始化代碼寫(xiě)在super.setUp()
后面,清理代碼要寫(xiě)在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ì)編寫(xiě)自己的測(cè)試方法冲杀。