XCTest

test(测试) 是你编写的代码,它可以操练应用程序和库代码,并根据一组期望值得出通过或失败的结果。test可能会在执行某些操作后检查对象实例变量的状态,验证代码在受边界条件约束时是否抛出特定异常,等等。对于性能测量测试,参考标准可以是一组例程运行到完成的最长时间。

为项目的组件设计的测试是test-driven development(测试驱动开发)的基础,这是一种编写代码的风格,在编写要测试的代码之前先编写测试逻辑。这种开发风格允许你在实现代码之前为代码编写需求和边缘用例。编写测试后,你将开发算法以通过测试。代码通过测试后,你便有了基础,可以对代码进行改进,并确信下次运行测试时会识别出对预期行为的任何更改(这会导致产品中的错误)。

即使你不使用test-driven development,测试也可以帮助你在修改代码以增强功能时减少错误的引入。将测试添加到未考虑测试目的的项目中,可能需要重新设计或重构部分代码,以使其更易于测试。

XCTest Framework

XCTest Framework 为 Xcode 项目创建和运行 Unit Tests、Performance Tests、UI Tests。

XCTestCase

XCTestCase是定义test cases(测试用例),test methods(测试方法)和performance tests的主要类。

通过编写一个或多个 test methods 将测试添加到你的 Xcode 项目中,每个 test methods 都验证代码的特定方面。将相关的 test methods 分组为 test cases,每个test cases都是 XCTestCase 的子类。

将 tests 添加到项目:

  1. 在测试目标内创建 XCTestCase 的新子类。
  2. 在test case中添加一种或多种test methods。
  3. 向每种测试方法添加一个或多个test assertions(测试断言)。
  • test methods 是 XCTestCase 子类上的实例方法,没有参数和返回值,名称以test开头。Xcode 中的 XCTest framework 会自动检测。test methods的命名要能明确说明该方法测试的内容,例如:testUserJSONFeedParsing()等。

  • test case 定义test case的名称要概括其中的测试,阐明测试的组织。例如:TableValidationTests、NetworkReachabilityTests或JSONParsingTests。

  • Tests assert 用于在test methods执行期间声明某些条件,如果这些条件不满足,则记录测试失败(并附带有可选消息),以确保你的代码行为符合预期。使用 XCTAssert 函数族检查布尔条件、nil或非nil值、期望值和抛出的错误。

XCTest Assertions

test methods使用XCTest framework提供的assertions(断言)来呈现Xcode显示的测试结果。所有assertions都具有类似的形式:要比较的项或逻辑表达式,失败结果格式字符串以及要插入格式字符串的参数。

test method可以包含多个assertions。如果Xcode包含的任何断言报告失败,则Xcode会发出测试方法失败的信号。

Assertions分为五类:

  1. Unconditional Fail(无条件失败),当仅到达特定的代码分支指示失败时,请使用此选项。此类别中唯一的断言是XCTFail。

  2. Equality Tests,使用这些来断言两个项目之间的关系。例如,XCTAssertEqual 断言两个表达式具有相同的值,而 XCTAssertEqualWithAccuracy 断言两个表达式在一定精度范围内具有相同的值。这一类还包括对不等式的检验,例如 XCTAssertNotEqual 和 XCTAssertNotEqual。

  3. Boolean Tests,使用这些来断言布尔表达式以某种方式计算,例如使用XCTAssertTrue或XCTAssertFalse。

  4. Nil Tests,使用这些来断言某项是否为nil,例如,使用XCTAssertNil或XCTAssertNotNil。

  5. Exception Tests,使用这些来断言评估表达式是否会生成异常。可以查找XCTAssertThrows引发的任何异常,也可以使用诸如XCTAssertThrowsSpecific的断言查找特定的异常。也可以断言相反,使用XCTAssertNoThrow之类的函数在计算表达式时不会引发异常。

Xcode Test Navigator

Xcode 的 test navigator(测试导航器) 和 integration reports(集成报告) 中使用了test case和test methods的名称来对测试进行分组和标识。

将test target添加到项目中会创建一个test bundle。test navigator对项目中所有test bundle的源代码组件进行布局,并在层次结构列表中显示test classes和test methods。

当你创建一个新项目时,默认情况下会为您创建一个test target和相关的test bundle,其名称是从你的项目名称派生的。

使用 Swift 编写测试

Swift的access control model(访问控制模型)阻止外部实体访问在应用程序或框架中声明为internal的任何内容。默认情况下,为了能够从测试代码访问这些项目,需要将它们的访问级别至少提高到public,从而降低Swift的类型安全性的好处。

Xcode提供了一个由两部分组成的解决方案:

  • 设置 test builds -> build setting -> Enable Testability -> Debug 设置为 YES,Xcode在编译过程中会包含-enable-testing标志。这使得在已编译模块中声明的Swift实体有资格获得更高级别的访问权限。
  • 将 @testable 属性添加到启用了测试功能的模块的import语句中时,可以激活该作用域中该模块的提升的访问权限。标记为internal或public的Classes 和 class members的行为就像标记为open的一样。其他标记为内部实体的实体就像被宣布为public实体一样。

如下所示:

1
2
import XCTest
@testable import MySwiftApp

@testable仅提供对internal函数的访问;使用@testable时,file-private和private声明在其通常作用域之外不可见。

1
2
3
4
5
6
7
class TestModel: NSObject {
private func sayHello() {
print("hello")
}

func doSome() { }
}
1
2
3
4
5
6
7
8
9
10
11
import XCTest
@testable import CTest

class ModelTests: XCTestCase {

func testExample() throws {
let m = TestModel()
m.doSome()
// m.sayHello() // 'sayHello' is inaccessible due to 'private' protection level
}
}

在 Xcode 中测试 App

XCTest使您能够以不同的抽象级别编写测试。好的测试策略应结合多种测试类型,以最大程度地发挥每种测试的优势。

如下图所示,旨在实现测试的“金字塔”分布。包括大量快速,隔离良好的unit tests,以涵盖应用程序的逻辑;少量的integration tests,以证明较小的部分正确地连接在一起;UI tests来assert(断言)常见用例的正确行为。

UI tests是你的应用以您期望的方式为用户工作的最终指标,但是与其他类型的测试相比,它们需要更长的运行时间。在同一个UI tests中,可能有多种应用程序变量导致失败。测试金字塔平衡了显示用户可以完成任务的高保真测试,以及针对性强的测试,这些测试可以为你提供有关应用程序逻辑的正确性以及所做更改的影响的快速反馈。

Unit Test

每个unit test都应该通过项目中的方法或函数assert单个路径的预期行为。要覆盖多个路径,请为每种情况编写一个test。例如,如果一个函数接收到一个可选参数,则可以编写一个参数为nil的test,以及一个参数为非nil的test。 确定代码中的边界情况和逻辑分支,并编写一个单元测试来覆盖这些情况的每种组合。

Unit Test侧重于代码功能,创建unit tests时,请专注于测试与Controller交互的代码的最基本基础,即Model类和方法。

test method应包含三个步骤,顺序如下所述:

1、Arrange(安排),创建你正在执行的代码路径使用的任何对象或数据结构。将复杂的依赖项替换为易于配置的 stubs,以确保测试快速运行并具有确定性。采用面向协议的编程可确保应用程序中对象之间的关系足够灵活,从而可以用stubs替换实际实现。

2、Act(行为),使用在Arrange阶段配置的参数和属性,调用要测试的方法或函数。

3、Assert(断言),使用XCTest framework中的Test Assertions来比较你在 Act 阶段执行的代码的行为与对应该发生的期望。任何条件为假的断言都会导致测试失败。

关于Unit Test的使用,具体可以学习 kakaopensource/KakaJSON,CoderMJLee 写的第三方框架都会写很多测试用例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import XCTest
@testable import CTest

class CTestTests: XCTestCase {

override func setUpWithError() throws {
// 在此处放置设置代码。在调用类中的每个测试方法之前调用此方法。
}

override func tearDownWithError() throws {
// 把清理代码放在这里。在调用类中的每个测试方法之后调用此方法。
}

/*
* 测试执行流程:对于每一个测试方法,都会分配一个类的新实例,并执行其实例 setup 方法。之后,它运行test method,然后运行实例 teardown 方法。此序列对类中的所有测试方法重复。
* 当Xcode运行测试时,它会独立调用每个测试方法。因此,每种方法都必须准备并清理与主题API交互所需的任何辅助变量,结构和对象。
*/
func testExample() throws {
// 这是一个功能测试用例的例子。
// 使用XCTAssert和相关函数来验证测试是否产生正确的结果。
}

// 为了处理调用异步执行方法和函数的测试组件,XCTest通过等待异步回调或超时完成,在测试方法中包含序列化异步执行的能力。
func testAsyncExample() {
// 测试将在此处暂停,运行运行循环,直到达到超时或所有期望都达到为止。
self.waitForExpectations(timeout: 1) { error in
// 在此处编写异步代码
}
}

// 性能测试采用你要评估的代码块并将其运行十次,以收集运行的平均执行时间和标准偏差。
// 这些单独测量值的平均值形成测试运行的值,然后可以将其与基准进行比较以评估成功或失败。
func testPerformanceExample() throws {
// 这是一个性能测试用例的示例
self.measure {
// 在此处放置你要测量时间的代码
}
}
}

Unit test for performance

编写performance tests,以收集有关在代码区域执行期间所花费的时间、所用内存、写入的数据的信息。

XCTest提供API来衡量基于时间的性能。 XCTest会多次运行你的代码,以测量所请求的metric(指标)。可以为metric(指标)设置baseline(基线)期望值,如果测量值明显低于baseline,则XCTest会报告测试失败。

当第一次运行performance tests时,XCTest总是报告失败,因为baseline未知。一旦你接受了某个测量作为baseline,XCTest将评估和报告成功或失败,并为你提供查看测试结果的详细方法。

要测试代码占用的时间,请在测试方法内调用measure(_:),然后在block参数中将应用的代码运行到measure(_:)。要使用其他指标(包括内存使用情况和写入磁盘的数据量)来衡量性能,请调用measure(metrics:block:)。

1
2
3
4
5
6
7
8
9
10
11
12
class ModelTests: XCTestCase {

func testSomePerformanceOfMyFunction() {
let model = TestModel()

// @metrics 接收你需要测量性能类型的数组:XCTMemoryMetric、XCTCPUMetric
self.measure(metrics: ModelTests.defaultMetrics) {
// 在此处放置你要测量时间的代码
model.myFunc()
}
}
}

Integration Test

Integration tests看起来与unit tests非常相似,使用相同的API,并遵循相同的Arrange-Act-Assert模式。unit test和integration test的区别在于scale(规模)。 unit test只占应用逻辑的一小部分,而integration test检查的是更大的子系统或类和函数组合的行为。在integration test的Arrange步骤中,使用较少的stub objects(存根对象)来扩展正在测试的实际项目代码的范围。

与其尝试像unit tests一样尝试覆盖所有不同的条件或边界情况,不如使用integration tests来assert组件在重要情况下协同工作以实现应用程序目标。示例包括测试从控制器接收的值是否正确存储在模型中,以及由网络请求产生的错误是否传递到用户界面并由用户界面呈现。

UI Test

UI tests使你能够查找应用程序的UI并与之交互,以验证elements的属性 和 状态。UI tests的重点是通过用户界面的流。

UI tests 包括 UI recording,它使你能够生成以与你的 App 相同的方式执行应用程序UI的代码,并且可以扩展该代码以实现UI tests。这是快速开始编写 UI tests 的好方法。

Test reports(测试报告) 得到了增强,可以提供有关UI tests的详细信息,包括测试失败时UI状态的快照。

UI tests 基于两项核心技术:XCTest framework 和 Accessibility(辅助功能)。

  • XCTest 提供了与Xcode集成的UI测试功能框架。创建和使用UI tests扩展了你对 XCTest 和创建unit tests的了解。创建UI test target,并在项目中创建UI测试的类和方法。可以使用XCTest assertions来验证预期结果是否正确。还可以通过Xcode Server和xcodebuild进行持续集成。

  • Accessibility是一项核心技术,可让残障用户获得与其他用户相同的iOS和macOS丰富体验。它包含有关UI的丰富语义数据集,用户可以使用这些语义数据来指导他们使用应用程序。Accessibility与UIKit和AppKit集成在一起,并具有API,可让你微调行为和对外公开的内容。UI tests使用该数据执行其功能。

用源代码创建UI tests类似于创建unit tests,它仍作为XCTestCase子类上的方法进行组织。为应用创建了一个UI test target;然后Xcode为你创建一个默认的UI test group和实现文件,并在实现文件中提供示例测试方法模板。创建UI test target时,可以指定测试将要处理的应用程序。

UI tests 的工作原理是:通过查询找到应用程序的UI对象,合成事件并将其发送到这些对象,并提供丰富的API,使你能够检查 UI对象的属性和状态,并将它们与预期状态进行比较。

UI tests 与unit tests的基本区别在于。unit tests使你可以在应用程序的范围内工作,并允许你在完全访问应用程序的的变量和状态的情况下使用函数和方法。UI tests以用户无法访问应用程序内部方法、功能和变量的方式练习应用程序的 UI。这使你的测试能够像用户一样查看应用程序,从而暴露用户遇到的 UI问题。

你的测试代码作为一个单独的程序运行,综合了应用程序中UI响应的事件。

UI tests 基于三个新类的实现:

  • XCUIApplication
  • XCUIElement
  • XCUIElementQuery
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class CTestUITests: XCTestCase {

override func setUpWithError() throws {
// 在此处输入设置代码。在调用类中的每个测试方法之前调用此方法。

// 在 UI tests 中,当出现故障时,最好立即停止。
// UI tests 方法中的每一步往往取决于前一步的成功;如果一个步骤失败,那么下面的所有测试也会失败。
continueAfterFailure = false

// 在 UI tests 中,必须先设置测试的初始状态(例如界面方向),然后再运行。setUp方法是执行此操作的好地方。
}

override func tearDownWithError() throws {
// 将清理代码放在此处。调用类中的每个测试方法后,将调用此方法。
}

func testExample() throws {
// UI tests 必须启动他们测试的应用程序。
let app = XCUIApplication()
app.launch()

// 使用 recording 开始编写 UI tests.
// 使用 XCTAssert 和相关函数来验证测试是否产生正确的结果。
}

func testLaunchPerformance() throws {
if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
// 这将测量启动应用程序所需的时间
measure(metrics: [XCTApplicationLaunchMetric()]) {
XCUIApplication().launch()
}
}
}
}

使用 UI Recording

从 UI recording 开始。它将源代码生成到一个测试实现文件中,可以编辑该文件来构建测试 或 回放特定的使用场景。UI recording 对于探索新的UI或学习如何编写UI测试序列也很有用。基本操作顺序是:

1、使用test navigator,创建UI tests target。

2、在创建的模板文件中,将光标置于测试函数中。

3、开始 UI recording。
应用程序启动并运行。使用应用程序以执行一系列UI操作。Xcode将操作捕获到函数体的源代码中。

4、完成要测试的操作后,停止UI recording。

5、向源代码添加 XCTest assertions。

编写 UI Tests

API tests 可以同时包含功能和性能方面,UI tests也可以。UI tests在App的表面空间运行,并且倾向于将许多低级功能集成到用户所看到的表示和响应中。

UI tests基本上是在事件和响应级别上操作的。

  • 查询以查找元素。
  • 知道元素的预期行为作为参考。
  • 轻触或单击该元素以引起响应。
  • 测量响应是否符合通过/失败结果的预期。

使用XCTest创建UI tests是与创建unit tests相同的编程模型的扩展。总体上使用了类似的操作和编程方法,不同之处在于UI tests API的基本概念以及它们在用户界面测试中描述的操作方式。

在测试类结构中,提供的setUp方法与单元测试类中的setUp包括两个区别。

在编写 UI tests 方法时,应该使用 UI recording 功能为测试创建一组基本步骤。然后可以使用XCTest assertions来为你的目的编辑此基本序列,以提供通过或失败结果(与unit tests一样)。``UI tests和unit tests`一样,既有功能方面,也有性能方面。

UI正确性测试的一般模式如下:

  • 使用 XCUIElementQuery 查找 XCUIElement。
  • 合成事件并将其发送到 XCUIElement。
  • 使用一个assertion 将 XCUIElement 的状态与预期的参考状态进行比较。

UI test for performance

要构建性能的UI test,请将可重复的UI步骤序列包装到test Performance中可见的measureBlock结构中。

代码覆盖范围

Code coverage(代码覆盖率) 是 Xcode 7 中的一项功能,使你可以可视化和测量测试执行了多少代码。使用code coverage,你可以确定测试是否在按预期的方式进行。

Xcode 中的 code coverage 是 LLVM 支持的一个测试选项。启用 code coverage 后,LLVM 会根据调用方法和函数的频率,对代码进行检测以收集覆盖率数据。code coverage选项可以收集数据以报告正确性和性能测试,无论是unit tests还是UI tests。

code coverage数据收集会导致性能下降。不管代价是否重大,它都将以线性方式影响代码的执行,因此启用它后,性能结果在测试运行与测试运行之间保持可比性。但是,在严格评估测试中例程的性能时,应考虑是否启用code coverage。

测试框架

specta/specta 和 specta/expecta 框架都是出自Cocoapods作者 orta。

facebookarchive/WebDriverAgent Archived,3.8k,自动化测试

学习博客

About Testing with Xcode

Testing Your Apps in Xcode

Using Unit Tests

Run UI tests and unit tests

onevcat * kiwi

iOS 单元测试和 UI 测试快速入门

Chars’s Blog * iOS 单元测试

使用 XCTest 消除动画卡顿

文章作者: Czm
文章链接: http://yoursite.com/2020/05/20/XCTest/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Czm!