跳转至

iOS单元测试和UI测试教程

2021年4月14日, Swift 5, iOS 14, Xcode 12**

了解如何在iOS应用中添加单元测试和UI测试,以及如何检查代码覆盖率。作者:David Piper

Download materials

Save for later

更新说明。David Piper针对Xcode 12.4、Swift 5.3和iOS 14更新了本教程。Audrey Tam撰写了原文。

iOS单元测试并不迷人,但由于测试使你闪亮的应用程序不会变成一个充满错误的垃圾,所以它是必要的。如果你正在阅读本教程,你已经知道你应该为你的代码和用户界面写测试,但你可能不知道如何写。

你可能有一个正在运行的应用程序,但你想测试你为扩展该应用程序所做的改变。也许你已经写好了测试,但不确定它们是否是正确的测试。或者,你已经开始在一个新的应用程序上工作,想要边做边测试。

本教程将告诉您如何。

  • 使用 Xcode 的测试导航器来测试应用程序的模型和异步方法
  • 通过使用存根和模拟来伪造与库或系统对象的交互
  • 测试UI和性能
  • 使用代码覆盖工具

在此过程中,您将掌握一些测试忍者所使用的词汇。

开始

首先使用本教程顶部或底部的Download Materials按钮下载项目资料。它包括项目BullsEye,该项目是基于UIKit Apprentice中的一个示例应用程序。这是一个简单的机会和运气的游戏。游戏逻辑在BullsEyeGame类中,你将在本教程中测试它。

搞清楚要测试什么

在编写任何测试之前,重要的是了解基本情况。你需要测试什么?

如果你的目标是扩展一个现有的应用程序,你应该首先为你计划改变的任何组件编写测试。

一般来说,测试应该涵盖。

  • 核心功能:模型类和方法以及它们与控制器的交互
  • 最常见的UI工作流程
  • 边界条件
  • 错误修复

了解测试的最佳实践

缩写FIRST描述了一套简洁的有效单元测试的标准。这些标准是。

  • 快速*。测试应该快速运行。
  • 独立/隔离*。测试不应该彼此共享状态。
  • 可重复。你应该在每次运行测试时获得相同的结果。外部数据提供者或并发问题可能导致间歇性的失败。
  • 自我验证。测试应该是完全自动化的。输出应该是 "通过 "或 "失败",而不是依赖程序员对日志文件的解释。
  • 及时性*。理想情况下,你应该在编写测试的生产代码之前编写你的测试。这就是所谓的测试驱动的开发。

遵循FIRST原则将保持你的测试清晰和有帮助,而不是变成你的应用程序的路障。

img

Xcode中的单元测试

测试导航器*提供了最简单的方法来处理测试。您将使用它来创建测试目标并针对您的应用程序运行测试。

创建一个单元测试目标

打开BullsEye项目,按Command-6打开Test navigator。

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

img

接受默认名称,BullsEyeTests,并输入com.raywenderlich作为组织标识符。当测试包出现在测试导航器中时,通过点击披露三角展开它,并点击BullsEyeTests在编辑器中打开它。

img

默认模板导入了测试框架XCTest,并定义了XCTestCaseBullsEyeTests子类,有setUpWithError()tearDownWithError()和示例测试方法。

你可以通过三种方式运行测试。

  1. Product ▸ TestCommand-U。这两种方式都可以运行所有测试类。
  2. 点击测试导航器中的箭头按钮。
  3. 点击沟槽中的钻石按钮。

img

你也可以通过点击测试导航器中的钻石或水沟中的钻石来运行一个单独的测试方法。

试着用不同的方法来运行测试,以了解它需要多长时间,以及它看起来是什么样子。样本测试还没有做任何事情,所以它们运行得非常快

当所有的测试都成功后,钻石会变成绿色,并显示复选标记。点击testPerformanceExample()末尾的灰色钻石,打开性能结果:

img

在本教程中,你不需要testPerformanceExample()testExample(),所以删除它们。

使用XCTAssert来测试模型

首先,你将使用XCTAssert函数来测试BullsEye模型的一个核心功能。BullsEyeGame是否正确计算了一个回合的分数?

BullsEyeTests.swift中,在import XCTest下面添加这一行:

@testable import BullsEye

这使得单元测试可以访问BullsEye中的内部类型和函数。

BullsEyeTests的顶部,添加这个属性:

var sut: BullsEyeGame!

这为BullsEyeGame创建了一个占位符,它是被测系统(SUT),或者说是这个测试用例类所关注的测试对象。

接下来,把setUpWithError()的内容替换成这样:

try super.setUpWithError()
sut = BullsEyeGame()

这在类的层面上创建了BullsEyeGame,所以这个测试类中的所有测试都可以访问SUT对象的属性和方法。

在你忘记之前,在tearDownWithError()中*释放你的SUT对象。将其内容替换为:

sut = nil
try super.tearDownWithError()

注意。在setUpWithError()中创建SUT并在tearDownWithError()中释放它是一个很好的做法,以确保每个测试都从一个干净的地方开始。关于更多的讨论,请查看Jon Reid的帖子关于这个问题。

编写你的第一个测试

现在你已经准备好写你的第一个测试了!

BullsEyeTests的末尾添加以下代码,以测试你是否计算出了猜测的预期分数:

func testScoreIsComputedWhenGuessIsHigherThanTarget() {
  // given
  let guess = sut.targetValue + 5

  // when
  sut.check(guess: guess)

  // then
  XCTAssertEqual(sut.scoreRound, 95, "Score computed from guess is wrong")
}

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

良好的做法是将测试格式化为givenwhenthen部分。

  1. 给定。在这里,你设置任何需要的值。在这个例子中,你创建了一个 "猜测 "值,这样你就可以指定它与 "目标值 "的差异程度。
  2. When: 在这一部分,你将执行被测试的代码。调用check(guess:)
  3. "然后"。这一部分你将断言你所期望的结果,并在测试失败时打印出一个信息。在本例中,sut.scoreRound应该等于95,因为它是100-5。

通过点击水沟中的钻石图标或在测试导航器中运行测试。这将建立并运行应用程序,钻石图标将变成一个绿色的复选标记 你还会看到Xcode上出现一个瞬间的弹出窗口,它也表示成功,看起来像这样。

img

注意。要查看XCTestAssertions的完整列表,请到Apple's Assertions Listed by Category

调试一个测试

在 "BullsEyeGame "中故意设置了一个错误,现在你将练习如何找到它。为了看到这个错误,你将创建一个测试,从given部分的`targetValue'中减去*5,其他部分保持不变。

添加以下测试:

func testScoreIsComputedWhenGuessIsLowerThanTarget() {
  // given
  let guess = sut.targetValue - 5

  // when
  sut.check(guess: guess)

  // then
  XCTAssertEqual(sut.scoreRound, 95, "Score computed from guess is wrong")
}

guesstargetValue之间的差异仍然是5,所以分数仍然应该是95。

在断点导航器中,添加一个测试失败断点。当一个测试方法发布一个失败的断言时,这将停止测试运行。

img

运行你的测试,它应该在XCTAssertEqual行停止,测试失败。

在调试控制台检查sutguess

img

guesstargetValue - 5,但scoreRound是105,不是95!

为了进一步调查,使用正常的调试过程。在when语句处设置一个断点,同时在BullsEyeGame.swiftcheck(guess:)内设置一个断点,在那里创建difference。然后,再次运行测试,跨过let difference语句,检查应用程序中difference的值:

img

问题是difference是负的,所以分数是100-(-5)。为了解决这个问题,你应该使用difference的绝对值。在check(guess:)中,取消对正确行的注释,删除不正确的一行。

删除两个断点,并再次运行测试以确认它现在成功了。

使用XCTestExpectation来测试异步操作

现在你已经学会了如何测试模型和调试测试失败,现在是时候继续测试异步代码了。

BullsEyeGame使用URLSession获得一个随机数作为下一场游戏的目标。URLSession方法是异步的。它们会立即返回,但要到后来才完成运行。为了测试异步方法,使用XCTestExpectation使你的测试等待异步操作的完成。

异步测试通常很慢,所以你应该把它们与你的快速单元测试分开。

创建一个名为BullsEyeSlowTests的新单元测试目标。打开全新的测试类BullsEyeSlowTests,并在现有的import语句下面导入BullsEye应用程序模块。

@testable import BullsEye

这个类中的所有测试都使用默认的URLSession来发送请求,所以声明sut,在setUpWithError()中创建它,在tearDownWithError()中释放它。要做到这一点,将BullsEyeSlowTests的内容替换为:

var sut: URLSession!

override func setUpWithError() throws {
  try super.setUpWithError()
  sut = URLSession(configuration: .default)
}

override func tearDownWithError() throws {
  sut = nil
  try super.tearDownWithError()
}

接下来,添加这个异步测试:

// Asynchronous test: success fast, failure slow
func testValidApiCallGetsHTTPStatusCode200() throws {
  // given
  let urlString = 
    "http://www.randomnumberapi.com/api/v1.0/random?min=0&max=100&count=1"
  let url = URL(string: urlString)!
  // 1
  let promise = expectation(description: "Status code: 200")

  // when
  let dataTask = sut.dataTask(with: url) { _, 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
  wait(for: [promise], timeout: 5)
}

这个测试检查发送一个有效的请求是否返回200状态代码。大部分的代码与你在应用程序中写的一样,只是多了这些行。

  1. expectation(description:): 返回XCTestExpectation,存储在promise中。description描述了你期望发生的事情。
  2. promise.fulfill(): 在异步方法的完成处理程序的成功条件闭合中调用这个,以标记期望已经被满足。
  3. wait(for:timeout:)。保持测试运行,直到所有的期望得到满足或`超时'间隔结束,以先发生者为准。

运行测试。如果你连接到互联网,在模拟器中加载应用程序后,测试应该需要大约一秒钟的时间才能成功。

快速失败

失败是很痛苦的,但它不一定要花很长时间。

要体验失败,只需将testValidApiCallGetsHTTPStatusCode200()中的URL改为无效的:

let url = URL(string: "http://www.randomnumberapi.com/test")!

运行该测试。它失败了,但它需要完整的超时间隔!这是因为你假设请求总是成功的,而你在这里调用了promise.fulfill()。这是因为你假设请求总是成功的,这就是你调用promise.fulfill()的地方。因为请求失败了,所以只有在超时时间过后才完成。

你可以通过改变假设来改善这一点,使测试更快地失败。与其等待请求成功,不如只等待异步方法的完成处理程序被调用。一旦应用程序从服务器上收到一个响应--无论是OK还是错误--就会发生,这就满足了期望。然后,你的测试可以检查请求是否成功。

要看这是如何工作的,创建一个新的测试。

但首先,通过撤销你对url所做的修改来修复之前的测试。

然后,在你的类中添加以下测试:

func testApiCallCompletes() throws {
  // given
  let urlString = "http://www.randomnumberapi.com/test"
  let url = URL(string: urlString)!
  let promise = expectation(description: "Completion handler invoked")
  var statusCode: Int?
  var responseError: Error?

  // when
  let dataTask = sut.dataTask(with: url) { _, response, error in
    statusCode = (response as? HTTPURLResponse)?.statusCode
    responseError = error
    promise.fulfill()
  }
  dataTask.resume()
  wait(for: [promise], timeout: 5)

  // then
  XCTAssertNil(responseError)
  XCTAssertEqual(statusCode, 200)
}

关键的区别是,只要进入完成处理程序就能实现预期,而这只需要一秒钟的时间。如果请求失败,then断言就会失败。

运行该测试。现在它应该需要一秒钟的时间来失败。它失败是因为请求失败,而不是因为测试运行超过了`timeout'。

修复url,然后再次运行测试,确认它现在成功了。

有条件地失败

在某些情况下,执行一个测试并没有什么意义。例如,当testValidApiCallGetsHTTPStatusCode200()在没有网络连接的情况下运行时会发生什么?当然,它不应该通过,因为它不会收到200状态代码。但它也不应该失败,因为它没有测试任何东西。

幸运的是,苹果公司引入了XCTSkip,在前提条件失败时跳过测试。在sut的声明下面添加以下一行:

let networkMonitor = NetworkMonitor.shared

NetworkMonitor包装了NWPathMonitor,提供了一个检查网络连接的方便方法。

testValidApiCallGetsHTTPStatusCode200()中,在测试的开始添加XCTSkipUnless

try XCTSkipUnless(
  networkMonitor.isReachable, 
  "Network connectivity needed for this test.")

XCTSkipUnless(_:_:)在没有网络的情况下跳过测试。通过禁用你的网络连接并运行测试来检查这一点。你会看到测试旁边的水沟里有一个新的图标,表示测试既没有通过也没有失败。

img

再次启用你的网络连接,重新运行测试,以确保在正常情况下仍然成功。在testApiCallCompletes()的开头添加同样的代码。

伪造对象和交互

异步测试可以让你相信你的代码会给异步API产生正确的输入。你可能还想测试一下,当你的代码从URLSession接收输入时,它是否能正确工作,或者是否能正确更新UserDefaults数据库或iCloud容器。

大多数应用程序与系统或库对象进行交互--你无法控制的对象。与这些对象交互的测试可能很慢,而且不可重复,违反了FIRST原则中的两项。相反,你可以通过从stubs获得输入或更新mock对象来*假的交互。

当你的代码对系统或库对象有依赖性的时候,就可以采用伪造的方式。通过创建一个假的对象来扮演这个角色,并将这个假的对象注入你的代码中。Jon Reid的Dependency Injection描述了几种方法来做到这一点。

伪造来自存根的输入

现在,检查应用程序的getRandomNumber(completion:)是否正确地解析了由会话下载的数据。你将用存根数据伪造BullsEyeGame的会话。

进入测试导航器,点击+,选择新建单元测试类...。给它取名为BullsEyeFakeTests,把它保存在BullsEyeTests目录下,并把目标设为BullsEyeTests

img

在 "import "语句下方导入BullsEye应用模块:

@testable import BullsEye

现在,将BullsEyeFakeTests的内容替换成这样:

var sut: BullsEyeGame!

override func setUpWithError() throws {
  try super.setUpWithError()
  sut = BullsEyeGame()
}

override func tearDownWithError() throws {
  sut = nil
  try super.tearDownWithError()
}

这声明了SUT,即 "BullsEyeGame",在 "setUpWithError() "中创建它,在 "tearDownWithError() "中释放它。

BullsEye项目包含支持文件URLSessionStub.swift。它定义了一个简单的协议,名为 "URLSessionProtocol",有一个方法来创建一个带有 "URL "的数据任务。它还定义了URLSessionStub,它符合这个协议。它的初始化器让你定义数据任务应该返回的数据、响应和错误。

要设置假货,请到BullsEyeFakeTests.swift并添加一个新的测试:

func testStartNewRoundUsesRandomValueFromApiRequest() {
  // given
  // 1
  let stubbedData = "[1]".data(using: .utf8)
  let urlString = 
    "http://www.randomnumberapi.com/api/v1.0/random?min=0&max=100&count=1"
  let url = URL(string: urlString)!
  let stubbedResponse = HTTPURLResponse(
    url: url, 
    statusCode: 200, 
    httpVersion: nil, 
    headerFields: nil)
  let urlSessionStub = URLSessionStub(
    data: stubbedData,
    response: stubbedResponse, 
    error: nil)
  sut.urlSession = urlSessionStub
  let promise = expectation(description: "Value Received")

  // when
  sut.startNewRound {
    // then
    // 2
    XCTAssertEqual(self.sut.targetValue, 1)
    promise.fulfill()
  }
  wait(for: [promise], timeout: 5)
}

这个测试做了两件事。

  1. 你设置了假数据和响应,并创建了假会话对象。最后,将假会话作为sut的一个属性注入到应用程序中。
  2. 你仍然要把这个写成一个异步测试,因为存根是假装成一个异步方法。通过比较targetValue和存根的假数字,检查调用startNewRound(completion:)是否解析了假数据。

运行该测试。它应该很快就能成功,因为没有任何真实的网络连接

伪造对模拟对象的更新

前面的测试使用了一个stub来提供来自一个假对象的输入。接下来,你将使用一个模拟对象来测试你的代码是否正确更新了`UserDefaults'。

这个应用程序有两种游戏风格。用户可以选择。

  1. 移动滑块以匹配目标值。
  2. 从滑块的位置猜测目标值。

在右下角的一个分段控制可以切换游戏风格,并将其保存在UserDefaults中。

你的下一个测试检查应用程序是否正确保存了gameStyle属性。

在目标BullsEyeTests中添加一个新的测试类,命名为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中声明SUT和模拟对象:

var sut: ViewController!
var mockUserDefaults: MockUserDefaults!

Replace setUpWithError() and tearDownWithError() with:

override func setUpWithError() throws {
  try super.setUpWithError()
  sut = UIStoryboard(name: "Main", bundle: nil)
    .instantiateInitialViewController() as? ViewController
  mockUserDefaults = MockUserDefaults(suiteName: "testing")
  sut.defaults = mockUserDefaults
}

override func tearDownWithError() throws {
  sut = nil
  mockUserDefaults = nil
  try super.tearDownWithError()
}

这将创建SUT和模拟对象,并将模拟对象作为SUT的一个属性注入。

现在,将模板中的两个默认测试方法替换成这样:

func testGameStyleCanBeChanged() {
  // given
  let segmentedControl = UISegmentedControl()

  // when
  XCTAssertEqual(
    mockUserDefaults.gameStyleChanged, 
    0, 
    "gameStyleChanged should be 0 before sendActions")
  segmentedControl.addTarget(
    sut,
    action: #selector(ViewController.chooseGameStyle(_:)),
    for: .valueChanged)
  segmentedControl.sendActions(for: .valueChanged)

  // then
  XCTAssertEqual(
    mockUserDefaults.gameStyleChanged, 
    1, 
    "gameStyle user default wasn't changed")
}

断言是,在测试方法改变分段控制之前,gameStyleChanged标志是0。因此,如果then*断言也是真的,这意味着set(_:forKey:)被精确调用了一次。

运行该测试。它应该成功。

Xcode中的UI测试

UI测试让您测试与用户界面的交互。UI测试的工作原理是通过查询找到一个应用程序的UI对象,合成事件,然后将事件发送到这些对象。该API使您能够检查一个UI对象的属性和状态,以比较它们与预期的状态。

在测试导航器中,添加一个新的UI测试目标。检查要测试的目标BullsEye,然后接受默认名称BullsEyeUITests

img

打开BullsEyeUITests.swift,在BullsEyeUITests类的顶部添加这个属性:

var app: XCUIApplication!

移除tearDownWithError()并将setUpWithError()的内容替换为以下内容:

try super.setUpWithError()
continueAfterFailure = false
app = XCUIApplication()
app.launch()

删除现有的两个测试,并添加一个新的测试,名为 "testGameStyleSwitch()"。

func testGameStyleSwitch() {    
}

testGameStyleSwitch()中打开一个新行,并点击编辑器窗口底部的红色记录按钮:

img

这将在模拟器中打开应用程序的模式,将你的互动记录为测试命令。一旦应用程序加载,点击游戏风格开关的滑动段和顶部标签。再次点击Xcode Record按钮来停止记录。

你现在在testGameStyleSwitch()中拥有以下三行:

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

记录器已经创建了代码来测试你在应用程序中测试的相同动作。向游戏风格的分段控制和顶部的标签发送一个点击。你将使用这些作为基础来创建你自己的UI测试。如果你看到任何其他的语句,只需删除它们。

第一行重复了你在setUpWithError()中创建的属性,所以删除这一行。你还不需要点击任何东西,所以也删除第2行和第3行末尾的.tap()。现在,打开["幻灯片"]旁边的小菜单,选择segmentedControls.butts["Slide"]

img

你应该剩下的是:

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

点任何其他对象,让记录器帮助你找到你可以在测试中访问的代码。现在,用这段代码替换这些行,创建一个*给定的部分:

// 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)
}

这将检查当你tap()分段控件中的每个按钮时,是否存在正确的标签。运行该测试--所有的断言都应该成功。

测试性能

来自苹果的文档

性能测试将你要评估的代码块运行10次,收集平均执行时间和运行的标准偏差。这些单独的测量值的平均值形成了一个测试运行的数值,然后可以与基线进行比较,以评估成功或失败

写一个性能测试很简单。只要把你想测量的代码放到measure()的闭合处即可。此外,你可以指定多个指标来测量。

BullsEyeTests中添加以下测试:

func testScoreIsComputedPerformance() {
  measure(
    metrics: [
      XCTClockMetric(), 
      XCTCPUMetric(),
      XCTStorageMetric(), 
      XCTMemoryMetric()
    ]
  ) {
    sut.check(guess: 100)
  }
}

这个测试测量多个指标:

  • XCTClockMetric测量经过的时间。
  • XCTCPUMetric跟踪CPU活动,包括CPU时间、周期和指令数量。
  • XCTStorageMetric告诉你被测试的代码向存储器写入多少数据。
  • XCTMemoryMetric跟踪使用的物理内存的数量。

运行测试,然后点击出现在measure()尾部闭合开始旁边的图标,查看统计数据。你可以改变Metric旁边的选定指标。

img

点击设置基线来设置一个参考时间。再次运行性能测试并查看结果--它可能比基线更好或更差。通过编辑按钮,你可以将基线重置为这个新的结果。

基线是按设备配置存储的,所以你可以让同一个测试在几个不同的设备上执行。每个设备可以根据具体配置的处理器速度、内存等保持不同的基线。

当你对一个应用程序进行修改,可能会影响到被测试方法的性能时,请再次运行性能测试,看看它与基线的比较情况。

启用代码覆盖

代码覆盖工具会告诉你,你的测试实际上在运行哪些应用程序的代码,所以你知道应用程序的哪些部分没有被测试--至少,还没有。

要启用代码覆盖,请编辑该方案的测试动作,并在选项标签下勾选收集覆盖的复选框:

img

Command-U运行所有测试,然后用Command-9打开报告导航器。在该列表的最上面的项目下选择覆盖率

img

点击披露三角形可以看到BullsEyeGame.swift中的函数和闭包列表:

img

滚动到getRandomNumber(completion:),可以看到覆盖率为95.0%。

点击该函数的箭头按钮,打开该函数的源文件。当你把鼠标移到右侧边栏的覆盖率注释上时,代码的部分会突出显示为绿色或红色:

img

覆盖率注释显示了测试对每个代码部分的调用次数。没有被调用的部分用红色标注。

实现100%的覆盖率?

你应该如何努力争取100%的代码覆盖率?只要谷歌一下"100%单元测试覆盖率",你就会发现一系列支持和反对的论点,以及对"100%覆盖率"的定义的争论。反对的观点认为,最后10%-15%的覆盖率不值得努力。支持的观点认为最后的10%-15%是最重要的,因为它很难测试。在谷歌上搜索"难以单元测试的坏设计",可以找到有说服力的论据,即无法测试的代码是更深层次设计问题的标志

从这里开始去哪里?

你可以使用本教程顶部或底部的下载材料按钮下载项目的完成版本。通过添加你自己的额外测试,继续发展你的技能。

你现在有了一些很好的工具,可以用来为你的项目编写测试。我希望这个iOS单元测试和UI测试教程能让你有信心去测试*所有的东西!

这里有一些资源供你进一步学习:

使用GitHub、Fastlane和Jenkins的持续集成以及Xcode Server for iOS:入门。

然后,看看苹果公司的Automating the Test Process与Xcode Server和xcodebuild,以及Wikipedia的持续交付文章,其中借鉴了ThoughtWorks的专业知识。

我们希望你喜欢这个教程,如果你有任何问题或评论,请加入下面的论坛讨论!