使用Xcode测试及XCTest框架

Xcode提供了XCTest框架用以编写测试代码。在创建Xcode工程时,Xcode默认使用XCTest,并且默认创建了Unit Test(单元测试)和 UI Test(界面测试)两个Target,我们可以直接使用。

Unit Test主要用于测试代码的大部分基本功能。比如绝大多数Model的类和方法测试,业务逻辑测试,网络接口调用测试等等。

UI Test一般会考虑到用户的交互流程,模拟用户的交互操作。UI Test利用XCTest的UI记录特性来获取界面上的一些列视图元素和操作事件,然后在测试方法中触发事件。

下面会介绍使用Xcode测试的基本流程,包含:创建测试目标、运行测试目标、测试类的结构、XCTest Framework 的主要类。Unit Test 和 UI Test 的内容也会详细介绍,还会介绍一下TDD (测试驱动开发) 和 BDD (行为驱动开发) 的基本概念。

1. 创建Test Target

虽然Xcode是默认创建Test Target的,但是我们可以自定义或者给没有Test Targe的工程添加Test Targe。在导航栏中切换到测试导航菜单,然后点击左下角+添加Test Target,如下图。


使用Xcode测试及XCTest框架_第1张图片
image.png

如果要在Test Target中创建新的Test Class,选中目标Test Target,使用File -> New -> File ,可以看到有UI Test Class和Unit Test Class两种类,如下图。

使用Xcode测试及XCTest框架_第2张图片
image.png

如此创建好 Test Target 和 Test Class之后,就可以在 Test Class 中编写测试代码了。

2. 运行Test

编写好测试代码之后就可以运行Test Target,Xcode既可以运行整个Target,也可以运行某一个Test Class,或者Test Class 中的一个方法。在测试导航栏中鼠标悬浮于具体目标,其右侧就会出现三角形按钮,点击后既可以运行。也可已在Test Class中的左侧看到整个类和方法左侧也有类似设置,鼠标炫悬浮出现三角形后可以点击运行。如图:

使用Xcode测试及XCTest框架_第3张图片
image.png

除此之外,菜单栏选择 Product -> Perform Action 也会出现 Testing Without Building, Test Again的选项,都可以有选择的运行。其中 Test Again 可以重复之前 Test 过的方法。

运行Test Targe时会按照scheme的设置运行,可以编辑scheme设置Test时的具体类容,包括configuration、 code coverage(如果勾选,在 Test 运行结束后会显示App中的代码覆盖率和方法调用次数)。同事可以勾选执行的Test Class 和 Test Method等。

使用Xcode测试及XCTest框架_第4张图片
image.png

运行之后的结果有多处可以查看。一个是在Test Class的measureBlock:^{
}];中查看,显示运行时长等信息如图:


使用Xcode测试及XCTest框架_第5张图片
image.png

还可以在导航栏的Report Navigator中查看Test结果。如图:


使用Xcode测试及XCTest框架_第6张图片
image.png

如果在Scheme中勾选了Code Coverge,在report中选择code coverge会出现下图效果,可以很清新的看到代码的覆盖了。在工程的Swif文件右侧,也可以看到每个方法的调用次数。


使用Xcode测试及XCTest框架_第7张图片
image.png

3. Test类

不管是Unit Test Class还是 UI Test Class 都继承自XCTestCase。Test Class中编写的测试方法都要以test开头,否则的话不会执行。因此,如果想暂时关闭测试方法,可以再方法名前面加一个Disable前缀,简洁明了,func DISABLE_testAddition() 这样。下图是一个Test Method 实现。

func testAddition() {
    app.buttons["6"].tap()
    app.buttons["+"].tap()
    app.buttons["2"].tap()
    app.buttons["="].tap()
    
    app.buttons["+"].tap()
    app.buttons["2"].tap()
    app.buttons["="].tap()
    
    if let textFieldValue = app.textFields["display"].value as? String {
        XCTAssertTrue(textFieldValue == "10", "Part 2 failed.")
    }
}

如果我们打开Test Class 会看到每个测试类都回重写setUp()、tearDown()方法。
其实所有的test方法是异步执行的,并且在执行之前都会调用setUp()方法,在执行之后都会调用tearDown()方法。因此有些重复的公有代码,可以放在这两个方法中, 比如启动程序,设置公有的测试模拟数据等。在测试的执行代码运行后,通常会使用XCAssert来断言运行结果,XCAssert会很明显的呈现测试的结果,一旦设置的断言条件不通过,就会中断并显示结果,有助于我们的定位问题。

对于Unit Test 和 UI Test,两者实现的方式是截然不同的。单元测试的实现方式是保证测试目标类的访问权限,实例化这个类 或者 模拟这个类 (使用类似 OCMock的方式 ),然后在测试方法中调用这个类需测试的方法、功能。而对 UI Test,则依靠XCFramework实现。利用XCUIApplication、 XCUIElement 、XCUIElementQuery来获取的App的各个UI对象,同步事件等等,并且将触发事件发送给UI对象。Unit Test 会访问目标类的内部方法,而UI Test则不会,只会在外部触发事件。

Unit Test 要提高测试目标类的访问权限在Swift有以下两种方式:一种是在Build Setting中设置Enable Testability为YES,另外一种是在import module之前加上@testable。

4. XCTest框架

这一部分只是一些简要的东西,具体内容还是要看官方文档。XCTest Framework是 UITest 实现的关键,主要由XCUIApplication XCUIElement XCUIElementQuery三个类以及XCUIElementAttributes XCUIElementTypeQueryProvider两个协议构成。

XCUIApplication类继承自XCUIElement类,XCUIElement类遵循XCUIElementAttributesXCUIElementTypeQueryProvider协议,而XCUIElementTypeQueryProvider协议返回的UI元素对象则是XCUIElementQuery类。

XCUIApplication实现application的launch、 terminal、 active、 state等功能, XCUIElement定义元素的操作事件,XCUIElementTypeQueryProvider协议实现查询所有的UI元素对象,XCUIElementAttributes则表示元素的属性,XCUIElementQuery表示元素的一些功能等等。

来看一个UI Test的实现,通过XCUIElementTypeQueryProvider 获取到UI 元素对象,并且向对象发送事件。

class CalcUITests: XCTestCase {

let app = XCUIApplication() //获取XCUIApplication对象
     
override func setUp() {
    super.setUp()
    
    continueAfterFailure = false
    XCUIApplication().launch() //XCUIApplication加载
}

override func tearDown() {
    super.tearDown()
}

func testAddition() {
    
     //查询到app的元素,并发送事件
    app.buttons["6"].tap()
    app.buttons["+"].tap()
    app.buttons["2"].tap()
    app.buttons["="].tap()
    
    if let textFieldValue = app.textFields["display"].value as? String {
        XCTAssertTrue(textFieldValue == "8", "Part 1 failed.")
    }
    
    app.buttons["+"].tap()
    app.buttons["2"].tap()
    app.buttons["="].tap()
    
    if let textFieldValue = app.textFields["display"].value as? String {
        XCTAssertTrue(textFieldValue == "10", "Part 2 failed.")
    }
}

5. Test持续集成

我们编写测试代码的最终目的其实是为了方便测试和修改,这样的话测试自动化即持续集成会带来很多益处。即时发现代码问题、保证主干版本质量、节约测试时间成本等等。因此将Test持续集成到CI工具是很好的做法。Xcode可以很好的和OS X Server结合使用,关联到 Git 版本库之后创建包含 Test 的bot,持续构建 测试 和 部署。

当然了你还可以使用xcodebuild命令行工具编写自动构建、测试脚本,结合Fastlane 和 Jenkins ,实现自动化测试。

6. TDD (测试驱动开发) 和 BDD (行为驱动开发)

6.1 TDD

TDD是一种相对于普通思维的方式来说,比较极端的一种做法。我们一般能想到的是先编写业务代码,然后为其编写测试代码,用来验证产品方法是不是按照设计工作。而TDD的思想正好与之相反,在TDD的世界中,我们应该首先根据需求或者接口情况编写测试,然后再根据测试来编写业务代码,而这其实是违反传统软件开发中的先验认知的。

在TDD原则的指导下,我们先编写测试代码。这时因为还没有对应的产品代码,所以测试代码肯定是无法通过的。在大多数测试系统中,我们使用红色来表示错误,因此一个测试的初始状态应该是红色的。接下来我们需要使用最小的代价(最少的代码)来让测试通过。通过的测试将被表示为安全的绿色,于是我们回到了绿色的状态。接下来我们可以添加一些测试例,来验证我们的产品代码的实现是否正确。如果不幸新的测试例让我们回到了红色状态,那我们就可以修改产品代码,使其回到绿色。如此反复直到各种边界和测试都进行完毕,此时我们便可以得到一个具有测试保证,扩展性超强的产品代码。在我们之后的开发中,因为你有这些测试的保证,你可以大胆重构这段代码或者与之相关的代码,最后只需要保证项目处于绿灯状态,你就可以保证代码没重构没有出现问题。

简单说来,TDD的基本步骤就是“红→绿→大胆重构”。

6.2 BDD

开始测试之旅并不是一件轻松的事,特别是在没有人帮助的情况下。也许你听闻 TDD 是多么有益,于是你坐在电脑前,打开 IDE, 为你其中一个组件建立了第一个测试文件。然后它就一片空白,也许你写了一些基本功能的测试,但是你总觉得哪里不对。有个问题始终潜伏在脑海深处。这个问题在真正前行之前必须要回答。

我应该测试些什么

像这样的测试有一个根本的问题:它们不会告诉你应该发生什么。它们也不会告诉你实际的预期是什么。它不清楚需求到底是什么。

此外,当一个测试失败,你必须深入代码并且理解为什么失败。这就需要大量额外不必要的认知负荷。在理想世界里,你不应该需要仅仅为了弄明白哪里出错了这种事情而花费如此大量的时间和精力。

这就是为什么会有行为驱动开发 (BDD),它旨在解决具体问题,帮助开发人员确定应该测试些什么。此外,它提供了一个 DSL(译者注: Domain-specific language,域特定语言)鼓励开发者弄清楚他们的需求,并且它引入了一个通用语言帮助你轻易理解测试的目的。

此深刻的问题的答案却惊人的简单,但是它需要改变你的对测试套件的看法。BDD 的第一个单词就表明了这一点,你不应该关注于测试,而是应该关注行为。这个看似毫无意义的变化提供了应该测试什么的准确答案:你应该测试行为。

但是什么是行为?好吧,为了回答这个问题,我们需要更技术一点。

让我们思考你设计的 app 中的一个对象。它有一个接口定义了其方法和依赖关系。这些方法和依赖,声明了你对象的约定。它们定义了如何与你应用的其他部分交互,以及它的功能是什么。它们定义了对象的行为。

同时这也应该是你的目标:测试你对象的行为方式

行为驱动开发(BDD)作为第二代敏捷方法,BDD提倡的是通过将测试语句转换为类似自然语言的描述,开发人员可以使用更符合大众语言的习惯来书写测试,这样不论在项目交接/交付,或者之后自己修改时,都可以顺利很多。如果说作为开发者的我们日常工作是写代码,那么BDD其实就是在讲故事。一个典型的BDD的测试用例包活完整的三段式上下文,测试大多可以翻译为Given..When..Then的格式,读起来轻松惬意。

下面是BDD框架Kiwi的一段测试代码,可以看到他的简单明了。

describe(@"Team", ^{
    context(@"when newly created", ^{
        it(@"should have a name", ^{
            id team = [Team team];
            [[team.name should] equal:@"Black Hawks"];
        });

        it(@"should have 11 players", ^{
            id team = [Team team];
            [[[team should] have:11] players];
        });
    });
});

我们很容易根据上下文将其提取为Given..When..Then的三段式自然语言

Given a team, when newly created, it should have a name, and should have 11 players

文章参考内容:
1.Testing with XCode
2.TDD的iOS开发初步以及Kiwi使用入门
3.行为驱动开发

你可能感兴趣的:(使用Xcode测试及XCTest框架)