在本課中片排,你將為FoodTracker應(yīng)用定義并測試一個數(shù)據(jù)模型(data model)怖竭。數(shù)據(jù)模型代表的是存儲在應(yīng)用中的信息的結(jié)構(gòu)惊楼。
學習目標
在本課結(jié)束的時候捻艳,你將能夠:
- 創(chuàng)建一個數(shù)據(jù)模型
- 為自定義類編寫可失敗的初始化器
- 從概念上理解可失敗和不可失敗初始化器之間的區(qū)別
- 通過編寫和運行單元測試來測試數(shù)據(jù)模型
創(chuàng)建數(shù)據(jù)模型
現(xiàn)在你將創(chuàng)建一個數(shù)據(jù)模型來存儲那些要在場景中顯示的信息。為了做到這一點,你定義一個包含名字(name)该溯、照片(photo)岛抄、評分(rating)的類。
創(chuàng)建一個新的數(shù)據(jù)模型類
- 選擇File > New > File (或者按下Command-N)狈茉。
- 在出現(xiàn)的對話框的頂部夫椭,選擇iOS。
- 選擇Swift 文件氯庆,點擊Next蹭秋。
由于你給數(shù)據(jù)模型定義了一個基類,意味著它不需要從其他類中繼承堤撵,所以它的創(chuàng)建方式和之前的RatingControl類創(chuàng)建方式不同仁讨。 - 在Save As字段,鍵入Meal实昨。
- 默認保存位置是你的項目目錄洞豁。
Group 選項默認是應(yīng)用名字,F(xiàn)oodTracker’荒给。在Targets部分丈挟,應(yīng)用被選中,而應(yīng)用冊測試沒有被選中志电。 - 其他的不變曙咽,點擊Create。
Xcode創(chuàng)建名為Meal.swift的文件挑辆。如有必要例朱,在Project navigator中,拖拽Meal.swift文件把它放置到其他Swift文件的下面之拨。
在Swift中茉继,你可以用String表示名字咧叭、用UIImage表示照片蚀乔、用Int表示評分。因為菜品總是有名字和評分菲茬,但不一定有照片吉挣,所以UIImage設(shè)置為可選(optional)。
為菜品定義數(shù)據(jù)模型
-
如果助理編輯器開著婉弹,則返回到標準編輯器睬魂。
- 打開Meal.swift。
- 改變import語句镀赌,用UIKit代替Foundation
import UIKit
當一個Xcode創(chuàng)建一個新swift文件氯哮,默認情況下會導入(import)Foundation框架,讓你在代碼中使用Foundation數(shù)據(jù)結(jié)構(gòu)商佛。由于你將要使用來自UIKit框架的類喉钢,所以你需要導入UIKit姆打。然而,導入了UIKit就能訪問Foundation肠虽,所以你可以刪除冗余的包含F(xiàn)oundation的代碼幔戏。
- 在import語句后面,添加如下代碼:
class Meal {
//MARK: Properties
var name: String
var photo: UIImage?
var rating: Int
}
這些代碼定義了你需要存儲的數(shù)據(jù)的基本屬性税课。你使用變量(var)來替代常量(let)是因為你將需要在Meal對象的整個生命周期中對它們進行修改闲延。
- 在屬性的下面,添加代碼來聲明一個初始化器韩玩。
//MARK: Initialization
init(name: String, photo: UIImage?, rating: Int) {
}
回想一下垒玲,初始化器方法能準備一個類的實例來使用,它需要為每個屬性設(shè)置一個初始化值找颓,并執(zhí)行任何其他的設(shè)置和初始化操作侍匙。
- 通過設(shè)置屬性等于參數(shù)值來填寫基本的實現(xiàn)。
// Initialize stored properties.
self.name = name
self.photo = photo
self.rating = rating
但是叮雳,如果你嘗試創(chuàng)建一個使用了不正確值的Meal將會發(fā)生什么想暗,比如給評分一個空值或者一個負值?你需要返回nil來表示這個項目不能被創(chuàng)建帘不,并已設(shè)置了一個默認值说莫。你需要添加代碼來檢查這種情況,如果失敗則返回nil寞焙。
- 緊跟著初始化存儲屬性代碼下面储狭,添加如下代碼:
// Initialization should fail if there is no name or if the rating is negative.
if name.isEmpty || rating < 0 {
return nil
}
這個代碼驗證傳入?yún)?shù),如果它們包含無效值則返回nil捣郊。
注意辽狈,編譯器會提示一個錯誤,“Only failable initializers can return nil (只有可失敗初始化器能返回nil)”
-
點擊錯誤圖標顯示fix-it信息呛牲。
- 雙擊fix it來更新你的初始化器」蚊龋現(xiàn)在初始化器應(yīng)該是這樣的:
init?(name: String, photo: UIImage?, rating: Int) {
可失敗初始化器總是使用init?或者init!。這些初始化器分別返回可選類型(optional)值或隱式解包可選類型(implicitly unwrapped optional)值娘扩∽湃祝可選類型能同時包含有效值和nil。你必須檢查是否可選類型有一個值琐旁,然后在使用之前安全的解包這個值涮阔。隱式解包可選類型也是可選類型,但是系統(tǒng)會對它們進行隱式解包灰殴。在本例中敬特,你的初始化器返回一個可選類型對象,Meal?
現(xiàn)在,你的init?(name: String, photo: UIImage?, rating: Int)初始化器看上去是這樣的:
init?(name: String, photo: UIImage?, rating: Int) {
// Initialization should fail if there is no name or if the rating is negative.
if name.isEmpty || rating < 0 {
return nil
}
// Initialize stored properties.
self.name = name
self.photo = photo
self.rating = rating
}
進一步探索
正如你在本系列課程后面看到的伟阔,可失敗初始化器較難使用尸变,因為你需要在使用之前對它的返回可選類型進行解包。一些程序員比較喜歡使用assert()和precondition()方法強行執(zhí)行初始化器减俏。這些方法在它們檢測到錯誤的時候終止應(yīng)用召烂。這意味著在調(diào)用初始化器之前,調(diào)用代碼必須有有效數(shù)據(jù)娃承。
更多關(guān)于初始化器的信息奏夫,查看Initialization。關(guān)于在代碼中添加內(nèi)聯(lián)性檢查和前提條件的信息历筝,查看assert(::file:line:)和precondition(::file:line:)
檢查點:通過選擇Product > Build(或按下Command-B)來構(gòu)建項目酗昼。你還沒有使用新類來做任何事情,但是構(gòu)建可以給編譯器一個機會來證實沒有輸入錯誤梳猪。如果你有麻削,根據(jù)編譯器提供的錯誤或警告信息來修復它,然后再回顧一下本課中的說明春弥,確保每件事都如它描述的那樣呛哟。
測試你的數(shù)據(jù)
雖然你的數(shù)據(jù)模型代碼已經(jīng)構(gòu)建,但是你還沒有把它并入到應(yīng)用中匿沛。因此扫责,很難判斷你已經(jīng)正確的實現(xiàn)了每件事,如你可能遇到在運行時未考慮到的邊緣情況逃呼。
為了解決這種不確定鳖孤,你可以寫單元測試。Unit tests(單元測試)是使用小型的抡笼、獨立的代碼片段苏揣,來確保它們的行為正確。Meal類是單元測試完美的候選人推姻。
查看FoodTracker的單元測試文件
-
在project navigator中點擊FoodTrackerTests文件旁的小三角來展開它平匈。
- 打開FoodTrackerTests.swift。
花一點時間來理解這個文件中迄今為止的代碼拾碌。
import XCTest
@testable import FoodTracker
class FoodTrackerTests: 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 testExample() {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
func testPerformanceExample() {
// This is an example of a performance test case.
self.measure {
// Put the code you want to measure the time of here.
}
}
}
代碼從導入(import)XCText框架到你的應(yīng)用開始吐葱。
注意街望,代碼在導入你的應(yīng)用的時候使用了@testable屬性校翔。這給了你的測試文件訪問你的應(yīng)用代碼內(nèi)部元素的入口。記住灾前,Swift默認對所有代碼中的類型防症、變量、屬性、初始化方法蔫敲、以及函數(shù)進行內(nèi)部訪問控制饲嗽。如果你沒有明確的標記一個項目是文件私有或私有,那么你就可以從測試訪問它奈嘿。
XCTest框架是Xcode的測試框架貌虾。單元測試本身在一個類中被定義,F(xiàn)oodTrackerTests裙犹,它繼承自XCTestCase尽狠。這些代碼注釋解釋了 setUp() 和 tearDown()方法,以及兩個測試用例:testExample() 和testPerformanceExample().
你能寫的測試的主要類型是函數(shù)測試(檢查它們是否能得到你期望的值)和性能測試(檢查代碼是否如你期望的那樣快)叶圃。因為你還沒有寫過任何很影響性能的代碼,所以你將只需要寫一些函數(shù)測試掺冠。
測試用例是簡單的方法,它們是作為單元測試的一部分由系統(tǒng)自動運行的德崭。為了創(chuàng)建測試用例,創(chuàng)建一個方法眉厨,方法名要以test開頭。最好給你的測試用例描述性的名字缺猛。這些名字可以讓你在以后很容易的識別單個測試。例如荔燎,一個測試檢查Meal類的初始化代碼,可以命名為testMealInitialization有咨。
為Meal對象初始化編寫單元測試
- 在 FoodTrackerTests.swift中,你不需要任何模版創(chuàng)建的方法座享。刪除這些模版方法。你的菜品跟蹤測試應(yīng)該是下面這樣的:
import XCTest
@testable import FoodTracker
class FoodTrackerTests: XCTestCase {
}
- 在結(jié)束的花括號之前渣叛,添加如下內(nèi)容:
//MARK: Meal Class Tests
- 在注釋下面,添加一個新的測試用例:
// Confirm that the Meal initializer returns a Meal object when passed valid parameters.
func testMealInitializationSucceeds() {
}
當單環(huán)測試運行的時候系統(tǒng)自動的運行這個測試用例蘑秽。
- 添加測試到測試用例饺著,測試使用0分和最高分來進行:
// Zero rating
let zeroRatingMeal = Meal.init(name: "Zero", photo: nil, rating: 0)
XCTAssertNotNil(zeroRatingMeal)
// Highest positive rating
let positiveRatingMeal = Meal.init(name: "Positive", photo: nil, rating: 5)
XCTAssertNotNil(positiveRatingMeal)
如果初始化器如預想般工作,則調(diào)用init(name:, photo:, rating:)將成功肠牲。XCTAssertNotNil通過檢查返回的Meal對象是否為nil來證明這一點幼衰。
- 現(xiàn)在在Meal類的初始化失敗的情況下添加測試用例。添加下面的方法到testMealInitializationSucceeds()方法下面缀雳。
// Confirm that the Meal initialier returns nil when passed a negative rating or an empty name.
func testMealInitializationFails() {
}
再次渡嚣,當單元測試運行的時候,系統(tǒng)會自動運行測試單元肥印。
- 現(xiàn)在添加測試代碼來測試使用無效參數(shù)調(diào)用初始化器的情況严拒。
// Negative rating
let negativeRatingMeal = Meal.init(name: "Negative", photo: nil, rating: -1)
XCTAssertNil(negativeRatingMeal)
// Empty String
let emptyStringMeal = Meal.init(name: "", photo: nil, rating: 0)
XCTAssertNil(emptyStringMeal)
如果初始化器如預想般工作,這些對init(name:, photo:, rating:)的調(diào)用會失敗竖独。XCTAssertNil通過檢查返回的Meal對象是否為nil來這時它裤唠。
- 到現(xiàn)在為止,這些測試都應(yīng)該是成功的∮。現(xiàn)在測試一個錯誤的情況种蘸。在負評分和空字符串測試代碼之間添加下面的代碼:
// Rating exceeds maximum
let largeRatingMeal = Meal.init(name: "Large", photo: nil, rating: 6)
XCTAssertNil(largeRatingMeal)
你的單元測試類應(yīng)該看上去是這樣的:
class FoodTrackerTests: XCTestCase {
你能添加額外的子類到你的FoodTrackerTests目標來添加額外的測試用例。選擇Product > Test (或者按下 Command-U)來同時運行所有的單元測試竞膳。你也可以運行一個單獨的測試航瞭。
檢查點:通過選擇Product > Test 菜單項運行單元測試。 testMealInitializationSucceeds()測試用例將成功坦辟,而testMealInitializationFails()測試用例會失敗刊侯。
注意Xcode在左側(cè)自動打開的Test navigator,高亮顯示失敗的測試锉走。
在編輯器窗口顯示當前打開文件的結(jié)果滨彻。在本例中,如果測試用例的一個或多個方法失敗的時候這個用例也就失敗挪蹭。如果測試方法的一個或多個測試失敗這個方法也就失敗亭饵。在本例中,只有XCTAssertNil(largeRatingMeal)測試失敗了梁厉。
Test navigator還列出了通過測試用例分組的各種測試方法辜羊。點擊測試方法可以在編輯器中導航到它的代碼。右側(cè)的圖標顯示了這個測試方法是成功還是失敗词顾。你能通過移動鼠標到成功或失敗的圖標上來返回一個測試方法八秃。當圖標變成一個播放箭頭圖標時,點擊它肉盹。
就像你看到的昔驱,單元測試幫助捕捉你代碼中的錯誤垮媒。它們還能幫你定義你的類期望的行為航棱。在本例中,Meal類的初始化器在你傳遞一個空字符串或者負評分時會失敗秕豫,但是傳遞一個大于5的值的時候不失敗观蓄。要回去修復它侮穿。
修改錯誤
- 在Meal.swift中亲茅,找到init?(name:, photo:, rating:)方法克锣。
- 你可以修改if子句,但是復雜的布爾表達式會讓理解變得困難验残。這里可以使用一系列檢查來替代它您没。而且胆绊,因為你在執(zhí)行代碼之前驗證數(shù)據(jù)辑舷,所以要使用guard語句。
guard(保護)語句聲明了一個條件肢础,這個條件必須為真传轰,以便執(zhí)行g(shù)uard語句后面的代碼被執(zhí)行谷婆。如果條件為假是,保護語句后面的else分支必須退出當前的代碼塊(例如跟匆,通過調(diào)用 return, break, continue, throw,或者一個類似fatalError(_:file:line:)不需要返回的方法)玛臂。
替換此代碼:
// Initialization should fail if there is no name or if the rating is negative.
if name.isEmpty || rating < 0 {
return nil
}
用下面的代碼:
// The name must not be empty
guard !name.isEmpty else {
return nil
}
// The rating must be between 0 and 5 inclusively
guard (rating >= 0) && (rating <= 5) else {
return nil
}
你的init?(name:, photo:, rating:)方法應(yīng)該看上去是這樣的:
init?(name: String, photo: UIImage?, rating: Int) {
// The name must not be empty
guard !name.isEmpty else {
return nil
}
// The rating must be between 0 and 5 inclusively
guard (rating >= 0) && (rating <= 5) else {
return nil
}
// Initialize stored properties.
self.name = name
self.photo = photo
self.rating = rating
}
檢查點:使用單元測試運行應(yīng)用迹冤。所有的測試用例都應(yīng)該通過泡徙。
單元測試是編寫代碼的重要部分堪藐,因為它幫助你不活你可能忽略的錯誤庶橱。就像它們的名字所示苏章,保持單元測試模塊化石重要的奏瞬。每個測試應(yīng)該檢查一個特定的基本類型行為。如果你寫長的復雜的單元測試并淋,就難以跟蹤錯誤县耽。
小結(jié)
在本課中兔毙,你構(gòu)建了一個模型(model)類來持有你的應(yīng)用數(shù)據(jù)澎剥。你還比較了常規(guī)初始化器和可失敗初始化器之間的區(qū)別哑姚。最后,你添加了幾個單元測試來幫助你找到代碼中的錯誤并修復它們倡蝙。
在稍后的課程中宛乃,你將在應(yīng)用的代碼中使用模型對象來創(chuàng)建和管理菜品列表征炼。但是谆奥,在你做這些之前拂玻,你需要學習如何使用表視圖(table view)來呈現(xiàn)菜品列表檐蚜。
注意
想看本課的完整代碼,下載這個文件并在Xcode中打開市栗。
下載文件