含义:编程语言中的单元测试是为了确保编写的代码按预期工作。
给定一个特定的输入,希望代码带有一个特定的输出。通过测试代码,能够给当前的重构和发布建立信心,因为将能够确保代码在成功运行的测试套件后按预期工作。
一、单元测试简介
单元测试是运行和验证一段代码(称为“单元”)以确保其按预期运行并符合其设计的自动化测试。
例如,写一个字符串扩展方法将第一个字母大写:
extension String {
func uppercasedFirst() -> String {
let firstCharacter = prefix(1).capitalized
let remainingCharacters = dropFirst().lowercased()
return firstCharacter + remainingCharacters
}
}
我们要确保 uppercasedFirst()方法按预期工作。如果我们给它一个输入 antoine,我们期望它输出 Antoine。我们可以使用XCTAssertEqual 方法为此方法编写单元测试:
final class StringExtensionsTests: XCTestCase {
func testUppercaseFirst() {
let input = "antoine"
let expectedOutput = "Antoine"
XCTAssertEqual(input.uppercasedFirst(), expectedOutput, "The String is not correctly capitalized.")
}
}
如果我们的方法不再按预期工作(比如上面的扩展代码不小心被修改了),Xcode 将使用我们提供的描述显示失败:
二、项目中添加单元测试
-
创建项目时勾选单元测试
-
已有项目添加测试target
左下角添加target,搜索test,选择Unit Test Bundle
-
项目中就会出现单元测试的文件夹
三、在 Swift 中编写单元测试
有多种方法可以测试相同的结果,但是当测试失败时它并不总是给出相同的反馈。以下提示可帮助您编写测试,通过从详细的失败消息中获益,帮助您更快地解决失败的测试。
1.命名测试用例和方法
描述你的单元测试是很重要的,这样你就会明白测试试图验证什么。如果你不能想出一个简短的名字,那你可能测试了太多东西。一个好名字还可以帮助您更快地解决失败的测试。
要快速找到特定类的测试用例,建议使用相同的命名并结合 “test”。就像上面的例子一样,我们根据我们正在测试一组字符串扩展的事实命名了 StringExtensionTests。如果您正在测试ContentViewModel 实例,另一个示例可能是 ContentViewModelTests。
2.不要所有测试都使用 XCTAssert
许多场景都可以使用 XCTAssert,但当测试失败时会导致不同的结果。以下代码行都测试了完全相同的结果:
func testEmptyListOfUsers() {
let viewModel = UsersViewModel(users: ["Ed", "Edd", "Eddy"])
XCTAssert(viewModel.users.count == 0)
XCTAssertTrue(viewModel.users.count == 0)
XCTAssertEqual(viewModel.users.count, 0)
}
正如你所看到的,该方法使用了一个描述性的名字,告诉人们要测试一个空的用户列表。然而,我们定义的视图模型不是空的,因此,所有的断言都失败了。
结果显示了为什么必须对验证类型使用正确的断言。 XCTAssertEqual 方法为我们提供了有关断言失败原因的更多上下文。这显示在红色错误和控制台日志中,可帮助您快速识别失败的测试。
3.Setup and Teardown
多个测试方法中使用的参数可以定义为测试用例类中的属性。您可以使用 setUp() 方法为每个测试方法设置初始状态,并使用 tearDown() 方法进行清理。有多种设置和拆卸方法的变体供您选择,例如支持并发的变体或抛出变体,如果设置失败,您可以在其中提前使测试失败。
一个可以生成用户默认实例以用于单元测试的示例:
struct SearchQueryCache {
var userDefaults: UserDefaults = .standard
func storeQuery(_ query: String) {
/// ...
}
}
final class SearchQueryCacheTests: XCTestCase {
private var userDefaults: UserDefaults!
private var userDefaultsSuiteName: String!
override func setUpWithError() throws {
try super.setUpWithError()
userDefaultsSuiteName = UUID().uuidString
userDefaults = UserDefaults(suiteName: userDefaultsSuiteName)
}
override func tearDownWithError() throws {
try super.tearDownWithError()
userDefaults.removeSuite(named: userDefaultsSuiteName)
userDefaults = nil
}
func testSearchQueryStoring() {
/// 使用生成的用户默认值作为输入。
let cache = SearchQueryCache(userDefaults: userDefaults)
/// ... write the test
}
}
这样做可以确保您不会操纵在模拟器上测试期间使用的标准用户默认值。其次,您将确保在测试开始时处于干净状态。我们使用了拆卸方法来删除用户默认套件并进行相应的清理。
4.抛出方法
和编写应用程序代码时一样,您也可以定义一个可抛出测试的方法。这允许您在测试中的方法抛出错误时使测试失败。例如,在测试 JSON 响应的解码时:
func testDecoding() throws {
/// 当数据初始值设定项抛出错误时,测试将失败。
let jsonData = try Data(contentsOf: URL(string: "user.json")!)
/// `XCTAssertNoThrow` 可用于获取有关抛出的额外上下文
XCTAssertNoThrow(try JSONDecoder().decode(User.self, from: jsonData))
}
当在任何进一步的测试执行中不需要 throwing 方法的结果时,可以使用 XCTAssertNoThrow 方法。您应该使用 XCTAssertThrowsError 方法来匹配预期的错误类型。例如,您可以为证书密钥验证程序编写测试:
struct LicenseValidator {
enum Error: Swift.Error {
case emptyLicenseKey
}
func validate(licenseKey: String) throws {
guard !licenseKey.isEmpty else {
throw Error.emptyLicenseKey
}
}
}
class LicenseValidatorTests: XCTestCase {
let validator = LicenseValidator()
func testThrowingEmptyLicenseKeyError() {
XCTAssertThrowsError(try validator.validate(licenseKey: ""), "An empty license key error should be thrown") { error in
/// 我们确保预期的错误被抛出。
XCTAssertEqual(error as? LicenseValidator.Error, .emptyLicenseKey)
}
}
func testNotThrowingLicenseErrorForNonEmptyKey() {
XCTAssertNoThrow(try validator.validate(licenseKey: "XXXX-XXXX-XXXX-XXXX"), "Non-empty license key should pass")
}
}
5.可选值解包
XCTUnwrap 方法最适合用于抛出测试,因为它是一个抛出断言:
func testFirstNameNotEmpty() throws {
let viewModel = UsersViewModel(users: [“Antoine”, “Maaike”, “Jaap”])
let firstName = try XCTUnwrap(viewModel.users.first)
XCTAssertFalse(firstName.isEmpty)
}
XCTUnwrap 断言可选变量的值不为 nil,如果断言成功则返回它的值。它会阻止您编写 XCTAssertNotNil 并结合解包或处理其余测试代码的条件链接。
四、在 Xcode 中运行单元测试
编写测试后,就该运行它们了。通过以下提示,这将变得更有效率。
1.使用测试三角形
您可以使用前导三角形运行单个测试或一组测试:
根据最新的测试运行结果,同一方块显示红色或绿色。
2.重新运行最新的测试
使用以下命令重新运行上次运行测试:
⌃ Control + ⌥ Option + ⌘ Command + G.
上面的快捷方式可能是我最常用的快捷方式之一,因为它可以帮助我在对失败测试实施修复后快速重新运行测试。
3. 运行测试组合
使用 CTRL 或 SHIFT 选择要运行的测试,右键单击并选择“Run X Test Methods”。
4.在测试导航器中应用过滤器
- 使用搜索字段根据名称搜索特定测试
- 仅显示当前所选方案的测试。如果您有多个测试方案,这将很有用。
- 只显示失败的测试。这将帮助您快速找到失败的测试
五、问题统计
1. 运行单元测试后代码签名失败
应用程序已经打开了自动签名,所以我认为当测试目标也没有打开自动签名时,Xcode 中出现了问题。代码签名需要一致。
2. 在单元测试中引用Framework
头部添加包名称@testable import (name)