转载

iOS 单元测试和UI测试教程

编写测试不是为了追求刺激,测试是为了避免你崭新的 App 变成了充满 bug 的垃圾,它是必须的。如果你正在阅读本教程,说明你已经意识到为你的代码和 UI 编写测试的重要性了,但你不一定知道怎么在 Xcode 中进行测试。

也许你已经有一个“功能正常”的 App 了,但根本没有为它进行过测试,你想在扩展它时能够测试这些修改。可能你也写过一些测试,但不知道这些测试是否正确。又或者,你正在编写 App,但想随时测试它。

本教程演示如何使用 Xcode 的测试导航器来测试 App 的模型或者异步方法,用 stubs 和 mocks 来模拟与框架或系统对象进行交互,如何测试 UI 及性能,以及如何使用代码覆盖工具。然后,你会学到测试狂人口中的一些词汇,在教程最后,你会充满自信地面对 SUT(被测系统)~

测试,测试……

什么是测试?

在编写测试之前,首先需要知道:你应该测试什么?如果你的目标是修改一个已有的 App,你首先应该对准备修改的组件编写测试。

通常,测试应当包含:

  • 核心功能:模型类和方法,以及它们和控制器的交互

  • 最常用的 UI 操作

  • 边际条件

  • bug 修复

重要的事情说三遍 —— FIRST 原则:测试的最佳实践

FIRST 是几个单词的缩写,简要描述了有效的单元测试需要什么条件,这些条件包括:

  • Fast:测试的运行速度要快,这样人们就不介意你运行它们了。

  • Independent/Isolated:一个测试不应当依赖于另一个测试。

  • Repeatable:同一个测试,每次都应当获得相同的结果。外部数据提供者和并发问题会导致间歇性的出错。

  • Self-validating:测试应当是完全自动化的,输出结果要么是 pass 要么是 fail,而不是依靠程序员对日志文件的解释。

  • Timely:理想情况下,测试的编写,应当在编写要测试的产品代码之前。

遵循 FIRST 原则会让你的测试清晰和有用,而不会成为 App 的渊薮。

开始

下载、解压缩和打开这个开始项目 BullsEye 和 HalfTunes。

BullsEye 是 iOS Apprentice 中的一个示例 App,我将游戏逻辑抽离到了一个 BullsEyeGame 类中,然后曾加了允许改变游戏风格的功能。

在右下角有一个 segmented 控件,允许玩家选择游戏风格:Slide,拖动 slider 能够尽可能地接近目标值,或者 Type,用猜的方式计算 slider 的位置。这个控件的 action 方法会保存用户选择的游戏风格到 user defaults 中。

HalfTunes 是 NSURLSession 教程 中的示例 App,已升级到 Swift 3。用户可以通过 iTunes API 搜索歌曲,下载和播放歌曲的片段。

开始测试吧!

用 Xcode 进行单元测试

创建一个单元测试 Target

Xcode 的测试导航器提供了一种最简单的进行测试的方法;你可以用它创建一个测试 target 并在你的 app 中进行测试。

打开 BullsEye 项目,按下 command+5 打开它的测试导航器。

点击左下角的 + 按钮,然后从菜单中选择 New Unit Test Target…:

iOS 单元测试和UI测试教程

使用默认的 BullsEyeTests 作为名字。当导航器中显示出测试 bundle 时,点击并在编辑器中打开它。如果 BullsEyeTests 没有自动显示,点击其它导航器,然后返回测试导航器。

iOS 单元测试和UI测试教程

模板代码中,import 了 XCTest,并定义了一个 XCTestCase 的继承类 BullsEyeTests,并声明了 setup()、tearDown() 和 示例测试方法。

有三种运行这个测试类的方法:

  1. Product/Test 或者 Command-U。这实际上会运行所有测试类。

  2. 点击测试导航器中的箭头按钮。

  3. 点击中缝上的钻石图标。

    iOS 单元测试和UI测试教程

你还可以点击某个测试方法上的钻石按钮单独测试这个方法,钻石按钮在导航器和中缝上都有。

测试不同的运行测试方法,看看它们会运行多长时间,以及运行起来的样子。示例测试不会执行任何动作,因此它们的运行是十分快速的!

如果所有测试通过,钻石会变绿并显示一个对勾。点击 testPerformanceExample() 底部的灰色钻石,打开 Performance Result:

iOS 单元测试和UI测试教程

你用不着 testPerformanceExample() 方法,请删除它。

用 XCTAssert 测试模型

首先,用 XCTAssert 测试 BullsEye 的模型中的一个核心功能:一个 BullsEyeGame 对象能够正确计算出一局游戏的得分吗?

在 BullsEyeTests.swift 中,在 import 语句下面添加:

@testable import BullsEye

这样单元测试就可以访问 BullsEye 中的类和方法了。

在 BullsEyeTests 类头部加入一个属性:

var gameUnderTest: BullsEyeGame!

在 setup() 方法中创建一个新的 BullsEyeGame 对象,位于 super 方法调用之后:

gameUnderTest = BullsEyeGame()
gameUnderTest.startNewGame()

这会用类的级别创建出一个 SUT(被测系统)对象,因此这个测试类中的所有测试都能够访问 SUT 对象的属性和方法。


这里,你也调用了游戏的 startNewGame 方法,这会创建一个 targetValue。你会有很多测试都要使用 targetValue,以测试游戏中计算的得分是否正确。

别忘了在 tearDown() 方法中释放你的 SUT 对象,在调用 super 方法之前:

gameUnderTest = nil


注意:在 setup() 中创建 SUT,在 tearDown() 中释放 SUT 是一种好的做法,能够保证每次测试都以干净的状态开始。更多讨论,请阅读 Jon Reid 的这篇帖子。

准备编写你的第一个测试了!

将 testExample() 方法修改为:

// 用 XCTAssert 测试模型
func testScoreIsComputed() {
  // 1. given
  let guess = gameUnderTest.targetValue + 5
  // 2. when
  _ = gameUnderTest.check(guess: guess)
  // 3. then
  XCTAssertEqual(gameUnderTest.scoreRound, 95, "Score computed from guess is wrong")
}

测试方法的名字总是以 test 开头,后面加上一个对测试内容的描述。

将测试方法分成 given、when 和 then 三个部分是一种好的做法:

  1. 在 given 节,应该给出要计算的值:在这里,我们给出了一个猜测数,你可以指定它和 targetValue 相差多少。

  2. 在 when 节,执行要测试的代码,调用 gameUnderTest.check(_:)方法。

  3. 在 then 节,将结果和你期望的值进行断言(这里,gameUnderTest.scoreRound 应该是 100-5),如果测试失败,打印指定的消息。

点击中缝上或者测试导航器上的钻石图标。App 会编译并运行,钻石图标会变成绿色的对勾!

注意:要查看完整 XCTestAssertions 列表,Command+左键,点击 XCTAssertEqual,将跳到 XCTestAssertions.h,或者通过阅读苹果的 Assertions Listed by Category。

iOS 单元测试和UI测试教程

注意:Given-When-Then 结构源自 BDD(行为驱动开发),是一个对客户端友好的、更少专业术语的叫法。另外也可以叫做 Arrange-Act-Assert 和 Assemble-Activate-Assert。

在测试中进行 debug

在 BullsEyeGame 中认为制造了一个 bug,你可以试试怎么找出它。将 testScoreIsComputed 修改为 testScoreIsComputedWhenGuessGTTarget,然后复制、粘贴,将它复制为另外一个方法 testScoreIsComputedWhenGuessLTTarget。

在这个方法中,在 given 节,让 targetValue - 5,而其它地方不变:

func testScoreIsComputedWhenGuessLTTarget() {
  // 1. given
  let guess = gameUnderTest.targetValue - 5
  // 2. when
  _ = gameUnderTest.check(guess: guess)
  // 3. then
  XCTAssertEqual(gameUnderTest.scoreRound, 95, "Score computed from guess is wrong")
}

猜的数和正确的数仍然相差 5,因此得分仍然应该是 95。

在断点导航器中,添加一个 Test Failure 断点,这样,当测试方法断言失败时,测试会停止。

iOS 单元测试和UI测试教程

运行测试,它会在 XCTAssertEqual 这行停止,同时报告测试失败。

查看 debug 控制台中的 gameUnderTest 和 guess 值:

iOS 单元测试和UI测试教程

guess 变量是 targetValue - 5,但 scoreRound 是 105,而不是 95!

接下来,用正常的调试方法进行调试:分别在 when 节的代码上设置一个断点,在 BullsEyeGame.swift 的 check(_:) 方法的声明 difference 变量处设置一个断点。再次运行测试,跳到 let difference 语句处,观察 difference 变量的值:

iOS 单元测试和UI测试教程

问题是: difference 是负值,因此 score 变成了 100 – (-5); 解决办法是在 difference 上取绝对值。在 check(_:) 方法中,反注释正确的代码,删掉错误的代码。

清除两个断点,再次运行测试确保测试通过。

用 XCTestExpectation 测试异步操作

现在我们学习了如何测试模型,如何对测试失败的情况进行 debug,接下来介绍用 XCTestExpectation 来测试网络操作。

打开 HalfTunes 项目:它使用 URLSession 来查询 iTunes API,下载歌曲小样。假设你想用 AlamoFire 来进行网络请求。为了证明一切正常,你应该为网络请求编写测试,并在修改代码之前进行测试。

URLSession 方法是异步的:它们会立即返回,但并不会终止运行直到未来某个时候。要测试异步方法,需要用 XCTestExpectation 以确保测试会等待异步操作完成。

异步测试通常是慢的,因此要和其它较快的单元测试分开来。

点 + 按钮,选择 New Unit Test Target… ,取名为 HalfTunesSlowTests。在 import 语句中,导入 HalfTunes:

@testable import HalfTunes

这个类中的测试方法会使用默认的 session 来向苹果服务器发送请求,因此声明一个 sessionUnderTest 对象,在 setup() 方法中实例化而在 tearDown() 中释放这个对象。

var sessionUnderTest: URLSession!
override func setUp() {
  super.setUp()
  sessionUnderTest = URLSession(configuration: URLSessionConfiguration.default)
}
override func tearDown() {
  sessionUnderTest = nil
  super.tearDown()
}

将 testExample() 方法修改为异步测试方法:

// 异步测试: 成功块,失败慢
func testValidCallToiTunesGetsHTTPStatusCode200() {
  // given
  let url = URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba")
  // 1
  let promise = expectation(description: "Status code: 200")
  // when
  let dataTask = sessionUnderTest.dataTask(with: url!) { data, response, error in
    // then
    if let error = error {
      XCTFail("Error: /(error.localizedDescription)")
      return
    } else if let statusCode = (response as? HTTPURLResponse)?.statusCode {
      if statusCode == 200 {
        // 2
        promise.fulfill()
      } else {
        XCTFail("Status code: /(statusCode)")
      }
    }
  }
  dataTask.resume()
  // 3
  waitForExpectations(timeout: 5, handler: nil)
}

这个方法检查 iTunes 返回状态码是否为 200。大部分代码和你在 App 中的写法一样,除了这几行:

  1. expectation(_:) 返回一个 XCTestExpectation 对象,这个对象保存了一个承诺(promise),又称作期望(expectation)或未来(future)。参数 description 用于描述你预期的行为。

  2. 为了和描述一致,你可以在异步方法的成功回调块中调用 promise.fullfill()。

  3. waitForExpections(:_handler:) 保持测试方法的运行,直到所有的 expectation 被 fullfill 或者到达指定超时时间,这两个条件任何一个都行。

运行测试。如果已连接网络,当模拟器中 app 启动之后,测试很快就会通过,

更快的失败

失败是痛苦的,但它不一定会发生。那么如何才能快速模拟测试失败的方法?这样我们才可以节省时间用于浪费到 Facebook 上?:]

可以修改你的代码,以便模拟异步操作失败的情况,将 URL 中 itunes 删除最后的 s:

let url = URL(string: "https://itune.apple.com/search?media=music&entity=song&term=abba")

运行测试:如果失败,app 会在超时时间之后才返回!这是因为它的 expectation 是请求成功,我们也只有在成功时才调用 promise.fullfil()。因此当操作失败时,测试要结束必须等超时时间过去。

你可以修改 expectation,让测试失败得更快一点:

我们不要去等待请求成功了,而是去等待异步方法完成块被回调的时候。这会在 app 收到响应之后立即发生——无论服务器返回的是 OK 还是错误,expectation 都会 fulfill。这样,无论请求是否成功,你都能检测到。

我们创建一个新的测试方法来进行测试。首先,将修改过的 url 恢复原样,然后新建一个测试方法:

// 异步测试: 让失败更快
func testCallToiTunesCompletes() {
  // given
  let url = URL(string: "https://itune.apple.com/search?media=music&entity=song&term=abba")
  // 1
  let promise = expectation(description: "Completion handler invoked")
  var statusCode: Int?
  var responseError: Error?
  // when
  let dataTask = sessionUnderTest.dataTask(with: url!) { data, response, error in
    statusCode = (response as? HTTPURLResponse)?.statusCode
    responseError = error
    // 2
    promise.fulfill()
  }
  dataTask.resume()
  // 3
  waitForExpectations(timeout: 5, handler: nil)
  // then
  XCTAssertNil(responseError)
  XCTAssertEqual(statusCode, 200)
}

关键的一点是,在完成回调中,简单地 fulfill 这个 expectation,这个过程会很快。如果请求失败,断言就会失败。

运行测试,现在失败的时间只需要一秒,这次的失败是真的因为请求失败,而不是因为超时。

恢复 url,再次进行测试,请求仍然是成功的。

模拟对象和交互

异步测试让你在编写调用异步 API 时更能保证输入的正确。你可能还想测试一下用 URLSession 获取数据,或者是更新 Userdefaults 或 CloudKit 数据库的代码是否正确。

大部分 App 会和系统或库对象打交道——你无法控制这些对象——和这些对象交互时测试会变慢和不可重现,这违背了 FIRST 原则的其中两条。但是,你可以通过从存根获取数据或者写入模拟对象写入来模拟这种交互。

当你的代码需要依赖系统或库对象时可以使用伪造手段——创建一个模拟的对象代替并将之注入到你的代码中。Jon Reid 的“依赖注入”一文介绍了几种方法。

模拟从存根获取数据

在这个测试方法中,你会测试 app 的 updateSearchResults(_:) 方法是否能够解析从 session 中下载的数据,通过检查 searchResults.cout 是否正确的方式。这个 SUT 是 view controller,你将用存根和已经下载好的数据来模仿 session。

点击 +,选择 New Unit Test Target…,取名为 HalfTunesFakeTest。在 import 语句中导入 HalfTunes:

@testable import HalfTunes

声明这个 SUT,在 setup() 方法中构建,在 tearDown() 方法中释放:

var controllerUnderTest: SearchViewController!
override func setUp() {
  super.setUp()
  controllerUnderTest = UIStoryboard(name: "Main", 
      bundle: nil).instantiateInitialViewController() as! SearchViewController!
}
override func tearDown() {
  controllerUnderTest = nil
  super.tearDown()
}

注意:SUT 是这个 view contoller,因为 HalfTunes 有一个很大的问题——所有的工作都是在 SearchViewController.swift 中进行的。将网络代码迁移到一个独立的模块中能够解决这个问题,并能使测试更容易进行。

然后,你需要一些测试的 JSON 数据,这些数据将通过伪造的 session 来提供给测试方法。只需要几个数据就可以了,你可以在 URL 字符串中用 &limit=3 来限制结果集:

https://itunes.apple.com/search?media=music&entity=song&term=abba&limit=3

将这个 URL 复制到浏览器中。将结果下载到一个 1.txt 之类的文件中。打开它,检查它的 JSON 格式,然后重命名为 abbaData.json,然后将它拖到 HalfTunesFakeTests 的文件组中。

HalfTunes 项目中有一个 DHURLSessionMock.swift 文件。它定义了一个简单的协议 DHURLSession,包含了用 URL 或者 URLRequest 来创建 data taks 的方法。还有实现了这个协议的 URLSessionMock 类,它的初始化方法允许你用指定的数据、response 和 error 来创建一个伪造的 URLSession。

构造模拟数据和 response,然后创建一个伪造的 seesion 对象,就在 setup() 方法中创建完 SUT 对象后面:

let testBundle = Bundle(for: type(of: self))
let path = testBundle.path(forResource: "abbaData", ofType: "json")
let data = try? Data(contentsOf: URL(fileURLWithPath: path!), options: .alwaysMapped)
let url = URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba")
let urlResponse = HTTPURLResponse(url: url!, statusCode: 200, httpVersion: nil, headerFields: nil)
let sessionMock = URLSessionMock(data: data, response: urlResponse, error: nil)

在 setup() 最后,将这个伪造的 session 注入到 app 的属性中:

controllerUnderTest.defaultSession = sessionMock

注意:你也可以直接在测试方法中使用这个伪造的 session,但我们想演示依赖注入,这样你就可以通过 view controller 的 defaultSession 属性来测试 SUT 的方法.

接下来准备编写测试方法,调用 updateSearchResults(_:) 方法来解析模拟数据。将 testExample() 方法替换为:

// 用 DHURLSession 协议和模拟数据伪造 URLSession
func test_UpdateSearchResults_ParsesData() {
  // given
  let promise = expectation(description: "Status code: 200")
  // when
  XCTAssertEqual(controllerUnderTest?.searchResults.count, 0, "searchResults should be empty before the data task runs")
  let url = URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba")
  let dataTask = controllerUnderTest?.defaultSession.dataTask(with: url!) {
    data, response, error in
    // 如果 HTTP 请求成功,调用 updateSearchResults(_:) 方法,它会将数据解析成 Tracks 对象
    if let error = error {
      print(error.localizedDescription)
    } else if let httpResponse = response as? HTTPURLResponse {
      if httpResponse.statusCode == 200 {
        promise.fulfill()
        self.controllerUnderTest?.updateSearchResults(data)
      }
    }
  }
  dataTask?.resume()
  waitForExpectations(timeout: 5, handler: nil)
  // then
  XCTAssertEqual(controllerUnderTest?.searchResults.count, 3, "Didn't parse 3 items from fake response")
}

我们仍然要编写异步测试的代码,因为存根是也模拟了异步方法。

当 data task 还没有执行,断言 searchResults 为空是成立的——即断言为 true,因为在 setup() 方法中我们创建的是一个全新的 SUT。

模拟数据中包含了 3 个 Track 对象,因此我们断言 view controller 的 searchResults 数组中包含了 3 个对象。

运行测试。很快就通过测试,因为根本就没有使用网络连接!

模拟写入 mock 对象

上一个测试使用了 stub (存根)来从伪造对象中获取数据。接下来,你将用一个伪造对象来测试向 UserDefaults 进行写入的代码是否正确。

再次打开 BullsEye 项目。这个 App 有两种游戏方式:用户可以拖动 slider 来猜数字,也可以通过 slider 的位置来猜数字。右下角的 segmented 控件可以切换游戏方式,并将 gameStyle 保存到 UserDefaults。

这个测试将检查 app 是否正确地保存了 gameStyle 到 UserDefaults 里。

在测试导航器中,点击 New Unit Test Target… ,取名为 BullsEyeMockTests。在 import 语句下添加:

@testable import BullsEye
class MockUserDefaults: UserDefaults {
  var gameStyleChanged = 0
  override func set(_ value: Int, forKey defaultName: String) {
    if defaultName == "gameStyle" {
      gameStyleChanged += 1
    }
  }
}

MockUserDefaults 重写了 set(_:forKey:) 方法,用于增加 gameStyleChanged 的值。通常你可能认为应当使用 Bool 变量,但使用 Int 能带来更多的好处——例如,在你的测试中你可以检查这个方法是否真的被调用过一次。

在 BullsEyeMockTests 中声明 SULT 和 MockUserDefaults 对象:

var controllerUnderTest: ViewController!
var mockUserDefaults: MockUserDefaults!

在 setup() 方法中,创建 SUT 和伪造对象,然后将伪造对象注入到 SUT 的属性中:

controllerUnderTest = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController() as! ViewController!
mockUserDefaults = MockUserDefaults(suiteName: "testing")!
controllerUnderTest.defaults = mockUserDefaults

在 tearDown() 中释放 SUT 和伪造对象:

controllerUnderTest = nil
mockUserDefaults = nil

将 testExample() 替换为:

// 模拟和 UserDefaults 的交互
func testGameStyleCanBeChanged() {
  // given
  let segmentedControl = UISegmentedControl()
  // when
  XCTAssertEqual(mockUserDefaults.gameStyleChanged, 0, "gameStyleChanged should be 0 before sendActions")
  segmentedControl.addTarget(controllerUnderTest, 
      action: #selector(ViewController.chooseGameStyle(_:)), for: .valueChanged)
  segmentedControl.sendActions(for: .valueChanged)
  // then
  XCTAssertEqual(mockUserDefaults.gameStyleChanged, 1, "gameStyle user default wasn't changed")
}

在测试方法“点击” segmented 控件之前,when 断言中,gameStyleChanged 的值应当是 0。在 then 断言中也是 true,因为 set(:forkey) 方法真的被调用了 1 次。

运行测试,测试通过。

XCode 的 UI 测试

从 Xcode 7 开始引入了 UI 测试,允许你通过记录 UI 上的交互来创建 UI 测试。UI 测试通过查询来找到 app 的 UI 对象,同步事件,然后将事件发送给这些对象。这个 API 允许你检索 UI 对象的属性和状态,并和预期值进行比对。

在 BullsEye 项目的测试导航器中,添加一个新的 UI Test Target。在 Target to be Tested 勾上 BullsEye,名称保持默认的 BullsEyeUITests。

在 BullsEyeUITest 类中添加一个属性:

var app: XCUIApplication!

在 setup() 方法,将 XCUIApplication().launch() 一句替换为:

app = XCUIApplication()
app.launch()

将 testExample() 方法名修改为 testGameStyleSwitch()。

在 testGameStyleSwitch() 中新起一行,然后在底部的编辑器窗口中,点击红色的 Record 按钮。

iOS 单元测试和UI测试教程

当模拟器中 App 一打开,点击游戏方式切换的 segmented 控件的 Slide 按钮和顶部标签。然后点击 Xcode 中的 Record 按钮,停止录制。

在 testGameStyleSwitch() 方法中会添加三行代码:

let app = XCUIApplication()
app.buttons["Slide"].tap()
app.staticTexts["Get as close as you can to: "].tap()

如果还有其它语句,请删除。

第一行代码完全和 setup() 方法中的代码重复了,同时你也不需要真的点击动作,因此可以删除第一行和其它两句的 .tap()。点开[“slide”]旁边的下拉菜单,选择

segsegmentedControls.buttons[“Slide”]。

这样就变成了:

app.segmentedControls.buttons["Slide"]
app.staticTexts["Get as close as you can to: "]

然后开始编写 given 节:

// given
let slideButton = app.segmentedControls.buttons["Slide"]
let typeButton = app.segmentedControls.buttons["Type"]
let slideLabel = app.staticTexts["Get as close as you can to: "]
let typeLabel = app.staticTexts["Guess where the slider is: "]

现在你已经获得两个按钮和两个顶部标签的引用,继续添加:

// then
if slideButton.isSelected {
  XCTAssertTrue(slideLabel.exists)
  XCTAssertFalse(typeLabel.exists)
  typeButton.tap()
  XCTAssertTrue(typeLabel.exists)
  XCTAssertFalse(slideLabel.exists)
} else if typeButton.isSelected {
  XCTAssertTrue(typeLabel.exists)
  XCTAssertFalse(slideLabel.exists)
  slideButton.tap()
  XCTAssertTrue(slideLabel.exists)
  XCTAssertFalse(typeLabel.exists)
}

这是为了测试当任何一个按钮被选中或点击时,对应的标签是否存在。运行测试,断言应当成立。

性能测试

根据苹果文档描述:性能测试将一段代码执行十遍,计算出平均执行时间和标准偏差。平均值用于和基线值进行比较,以衡量是否测试通过。

编写性能测试十分简单:将需要测试的代码放进 measure() 方法的闭包中。

要实际体验一把,可以打开 HalfTunes 项目,将 HalfTunesFakeTests 的 testPerformanceExample() 方法替换为:

// 性能测试 
func test_StartDownload_Performance() {
  let track = Track(name: "Waterloo", artist: "ABBA", 
      previewUrl: "http://a821.phobos.apple.com/us/r30/Music/d7/ba/ce/mzm.vsyjlsff.aac.p.m4a")
  measure {
    self.controllerUnderTest?.startDownload(track)
  }
}

运行测试,点击 measure() 方法闭包结束 } 旁边的图标,查看统计结果。

iOS 单元测试和UI测试教程

点击 Set Baseline,再次运行测试,查看结果——结果可能比基线要好或差。 Edit 按钮允许你修改基线以查看新的报告。

基线是根据设备配置来保存的,因此你可以在不同设备上进行同一个测试,每种设备会根据其配置的处理器速度、内存而拥有不同的基线。

当你对 App 进行修改之后,都会对测试性能造成影响,请重新运行性能测试,查看时需要参照基线进行。

代码覆盖

代码覆盖工具会报告 App 代码有多少经过了测试,你也可以知道那一部分代码还没有经过测试。

注意:在代码覆盖选项打开的情况下,可以运行性能测试吗?根据苹果文档中描述:代码覆盖率统计会导致性能下降……它以线型方式影响代码的执行,因此当同样是在打开的情况下,一次性能测试的结果和另外一次性能测试的结果仍然是有可比性的。但是,如果要在测试中进行严谨的性能计算,你应当考虑是否要开启代码覆盖。

要打开代码覆盖,编辑 scheme 中的 Test,然后勾选 Code Coverage 选项:

iOS 单元测试和UI测试教程

运行全部测试(command+U),打开报告导航器(command+8)。选择 By Time,选择列表中第一个,然后选择 Coverage 栏:

iOS 单元测试和UI测试教程

点击倒三角,查看 SearchViewController.swift 的函数列表:

iOS 单元测试和UI测试教程

将鼠标放到 updateSearchResults(:) 旁边的蓝色覆盖率统计条上,可以看到覆盖率为 71.88%。

点击这个函数的箭头按钮,打开源文件,找到该函数。将鼠标靠近右边缝的覆盖率标注上,代码会被分成不同颜色的部分,红色或者绿色:

iOS 单元测试和UI测试教程

股概率标注显示了一个测试“击中”了代码多少次,没有被调用过的代码会用红色标注。你可能猜到了,for 循环被执行了 3 次,而错误路径一次都没执行。为了提高这个函数的覆盖率,你应该复制 abbaData.json,修改内容使它产生错误——比如,将“results”修改为“result”,从而导致这句:print(“Results key not found in dictionary”)被调用。

100% 的覆盖率

要追求 100% 的覆盖率有多难?你可以用谷歌搜一下 100% unit test coverage,你会发现有许多赞成和反对的意见,以及关于对 100% 股概率的定义的争论。反对的一方认为至少有 10-15% 的覆盖率是毫无意义的。赞成的一方认为最后的 10-15% 是最重要的,因为它们很难被测试出来。再搜一下 “hard to unit test bad design”,你会发现一种比较有说服力的说法,不能测试的代码表明存在深层次的设计问题。进一步的思考表明,测试驱动开发才是正理。

正文到此结束
Loading...