重要:这是针对于正在开发中的API或技术的预备文档(预发布版本)。苹果提供这份文档的目的是帮助你按照文中描述的方式对技术的选择及界面的设计开发进行规划。这些信息有可能发生变化,因此根据本文档的软件开发应当基于最终版本的操作系统和文档进行测试。该文档的新版本或许会随着API或相关技术未来的发展而进行更新。
翻译自苹果官网:
在本课中,你将为 FoodTracker app 定义一个数据模型。这个数据模型代表 app 的信息结构。
学习目标
在课程的最后,你将能够:
- 创建一个数据模型
- 在自定义类中编写可失败构造器
- 理解可失败和不可失败构造器的区别
- 通过编写和运行单元测试来测试一个数据模型
创建一个数据模型
现在创建一个数据模型来存储食物场景需要展示的信息。定义一个简单的拥有 name 属性、 photo 属性和 rating 属性的类来表示这个模型。
创建一个数据模型类
选择 File > New > File (或者按 Command-N)。
在出现的对话框的左边,选择 iOS 下面的 Source。
选择 Swift 文件,然后点击 Next。
你正在使用一种不同与先前创建 RatingControl 类的步骤来创建类 (iOS > Source > Cocoa Touch Class),因为你为你的数据模型定义了一个基类,这意味着它不需要继承任何其他类。在 Save As 区域,输入 Meal。
-
保存位置默认是你的项目目录。
Group 选项默认是你的 app 名字,FoodTracker。
在 Targets 区域,确保 app 和 test 都选中了。[图片上传失败...(image-2a95c3-1608214873246)]
点击 Create。
Xcode 创建一个叫做 Meal.swift 的文件。
在 Swift 中,使用一个 String 来表示名字,使用一个 UIImage 来表示照片,使用一个 Int 来表示评分。因为食物经常会拥有名字和评分,但是可能没有照片,所以让 UIImage 为可选类型。
为食物定义一个数据模型
-
如果辅助编辑器是打开状态,通过点击 Standard 按钮来返回标准编辑器。
[图片上传失败...(image-204b91-1608214873246)]
打开 Meal.swift。
-
修改 import 语句来引入 UIKit 代替 Foundation:
import UIKit
默认 Swift 文件引入 Foundation 框架所以可以直接使用其中的数据结构。因为要使用 UIKit 的类,所以需要 import UIKit。导入 UIKit 同时让你能访问 Foundation,所以可以移除多余的 import Foundation。
-
在 import 语句的下面,添加如下代码:
class Meal { // MARK: Properties var name: String var photo: UIImage? var rating: Int }
代码为需要储存的数据定义了基础属性。使用变量(var)而不是常量(let)因为它们在食物对象的生命周期过程中需要修改。
-
在属性的下面,添加代码来定义一个构造器:
// MARK: Initialization init(name: String, photo: UIImage?, rating: Int) { }
回忆一下构造器是准备一个类的实例的方法,它为每个属性设置初始值并执行任何其他的初始化和构造过程。
-
设置属性等于参数值。
// Initialize stored properties. self.name = name self.photo = photo self.rating = rating
但是当你尝试创建一个不正确的食物会发生什么,例如一个空的名字或者一个负的评分?你需要添加代码来检查这些情况并返回 nil 来表示构造失败了。
-
在构造器的最后,添加 if 语句来检查不正确的值并且当一个条件不满足就返回 nil。
// Initialization should fail if there is no name or if the rating is negative. if name.isEmpty || rating < 0 { return nil }
-
点击错误 fix-it 来添加一个问号(?)到构造器的 init 关键字后面。
[图片上传失败...(image-52b767-1608214873246)]
这被称为可失败构造器,这意味着构造器有可能返回 nil 值。
此刻,init?(name:photo:rating:) 构造器应该像下面这样:
// MARK: Initialization
init?(name: String, photo: UIImage?, rating: Int) {
// Initialize stored properties.
self.name = name
self.photo = photo
self.rating = rating
// Initialization should fail if there is no name or if the rating is negative.
if name.isEmpty || rating < 0 {
return nil
}
}
检验:通过选择 Product > Build(或者按 Command-B)来编译项目,不要使用你新创建的类做任何事情,只是编译它给编译器一次机会来验证你没有犯任何的输入错误,像问号(?)。如果你犯错了,通过阅读编译器提供的警告和错误来解决问题,之后回顾过去课程的说明来确保一切都看起来像这里描绘的样子。
测试你的数据
尽管你的数据模型代码完成了,但你并没有把它结合到你的 app 中。所以很难说明是否已经正确实现一切了,同时可能在运行时遇到并没有考虑到的临界情况。
可以编写单元测试来定位这些不确定因素。它用于测试少量且独立的代码来确保它们功能正确。食物类是个完美的单元测试用例。
Xcode 已经创建一个单元测试文件作为 Single View Application 模板的一部分。
为 FoodTracker 查看单元测试文件
-
通过点击项目导航中 FoodTrackerTests 文件夹旁边的三角形打开它。
[图片上传失败...(image-c70d3a-1608214873246)]
打开 FoodTrackerTests.swift。
花一点时间理解目前的代码;
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.
XCTAssert(true, "Pass")
}
func testPerformanceExample() {
// This is an example of a performance test case.
self.measureBlock() {
// Put the code you want to measure the time of here.
}
}
}
这个文件引入了 XCTest 框架。单元测试定义在一个叫 FoodTrackTests 的类中,它继承自 XCTestCase。代码注释解释了 setUp() 和 tearDown() 方法。
你可以写的主要类型的测试是功能测试(为了检查一切正在产生你期望的值)和性能测试(为了检查你的代码执行的是否像你预期的那么快)。因为你没有编写任何很影响性能的代码,你现在仅仅需要编写功能测试方法。
使用 test 作为标题开始任一你想要用作测试的方法,给它一个特殊的标题这样之后就更容易识别了,例如,一个测试方法或许检查食物是否得到正确的初始化,你可以把它命名为 testMealInitialization。
为食物对象构造过程编写单元测试
-
删除 FoodTrackerTests.swift 中模板创建的测试方法。
import UIKit import XCTest class FoodTrackerTests: XCTestCase { }
-
在最后的大括号(})前面,添加如下注释:
// MARK: FoodTracker Tests
它帮助你(或其他任何读你的代码的人)在测试方法中间导航和标识所对应的模块。
-
在注释下面,添加一个新的单元测试方法:
// Tests to confirm that the Meal initializer returns when no name or a negative rating is provided. func testMealInitialization() { }
-
首先,添加一个能通过的测试用例。向 testMealInitialization() 测试方法中添加如下注释和代码行:
// Success case. let potentialItem = Meal(name: "Newest meal", photo: nil, rating: 5) XCTAssertNotNil(potentialItem)
XCAssertNotNil 测试食物对象在初始化后不为空,这意味着使用提供的参数成功地初始化并创建了一个食物对象。
-
现在添加一个食物对象初始化失败的测试用例。向 testMealInitialization() 测试方法添加如下注释和代码行。
// Failure cases. let noName = Meal(name: "", photo: nil, rating: 0) XCTAssertNil(noName, "Empty name is invalid")
XCTAssertNil 断言一个对象是 nil。在这个例子中,意味着 noName 对象是 nil,说明构造失败。你预期构造失败因为名字是空的字符串,这是你明确设置用于测试的构造器。
-
现在添加一个食物对象初始化失败的测试用例,但是这次,尝试断言初始化成功。向 testMealInitialization() 测试方法添加如下代码:
let badRating = Meal(name: "Really bad rating", photo: nil, rating: -1) XCTAssertNotNil(badRating)
你预期这个测试用例会失败因为评分是负的,这是你在构造方法中明确设置用于测试的。
testMealInitialization() 单元测试方法应该像这样:
// Tests to confirm that the Meal initializer returns when no name or a negative rating is provided.
func testMealInitialization() {
// Success case.
let potentialItem = Meal(name: "Newest meal", photo: nil, rating: 5)
XCTAssertNotNil(potentialItem)
// Failure cases.
let noName = Meal(name: "", photo: nil, rating: 0)
XCTAssertNil(noName, "Empty name is invalid")
let badRating = Meal(name: "Really bad rating", photo: nil, rating: -1)
XCTAssertNotNil(badRating)
}
你可以按 Command-U 同时运行你的所有单元测试方法,或者你可以运行一个单独的测试方法。最后的测试用例预期会失败因为尽管实际上是 nil 但你断言对象是非空的。
运行 testMealInitialization() 单元测试方法
找到 FoodTrackerTests.swift 中的 testMealInitialization() 单元测试方法。
-
在测试方法名字左边,找到一个菱形形状。
[图片上传失败...(image-93d313-1608214873246)]
-
悬停你的鼠标在菱形上面会露出一个小的 Run 按钮。
[图片上传失败...(image-5dbe27-1608214873246)]
点击 Run 按钮来运行单元测试方法。
检验:运行刚才写的单元测试方法。前两个测试用例应该能通过,最后一个应该失败。
[图片上传失败...(image-ba12eb-1608214873246)]
你会看到,单元测试帮助在代码中捕捉错误。如果你实际上期望最后测试用例中的食物对象非空,你在测试中不会捕捉到这个错误。(在这个例子中,因为你故意编写一个失败的测试用例,你仅仅只需返回你的代码来修复你的测试用例。)
修复测试用例
找到 FoodTrackerTests.swift 中的 testMealInitialization() 单元测试方法。
-
修改最后一行为这个:
XCTAssertNil(badRating, "Negative ratings are invalid, be positive")
testMealInitialization() 单元测试方法应该像这样:
// Tests to confirm that the Meal initializer returns when no name or a negative rating is provided.
func testMealInitialization() {
// Success case.
let potentialItem = Meal(name: "Newest meal", photo: nil, rating: 5)
XCTAssertNotNil(potentialItem)
// Failure cases.
let noName = Meal(name: "", photo: nil, rating: 0)
XCTAssertNil(noName, "Empty name is invalid")
let badRating = Meal(name: "Really bad rating", photo: nil, rating: -1)
XCTAssertNil(badRating, "Negative ratings are invalid, be positive")
}
检验:运行刚才写的单元测试方法。所有的测试用例应该能通过。
[图片上传失败...(image-78e1b3-1608214873247)]
单元测试是编写代码中一个必要的环节因为它能帮助你捕捉一些你或许会忽略的错误。就像它们的名字所表达的,保持单元测试模块化非常重要。每个测试应该检查一个特别而且基础的一类行为。如果你编写的单元测试非常长或者复杂,这会很难追踪到底发生了什么的。
注意:
为了看到本课的完整示例项目,下载文件并在 Xcode 中查看它。