iOS 之性能测试、UI测试

测试时从何处开始

当我们开始测试时,应当遵守以下原则:

  • 当创建单元测试时,应当着重于代码中最基础的部分,即与 Controller交互的Model类和方法。
  • 当创建UI测试时,首先考虑最常见的工作流程。想象下用户开始使用app时会做些什么以及过程中执行了哪些UI。使用UI recording 功能可以捕捉用户的一系列动作到测试方法中,也可以扩展该方法来验证测试的正确性或性能。

创建一个测试类

我们可以使用导航栏下方的加号按钮创建一个新的测试类

test-0
test-1.png

也可以使用command + N的快捷键方式来创建

test-2

上图选择的分别的UI测试和单元测试。

注意: 所有的测试类都是XCTest框架的XCTestCase的子类

测试类的结构

测试类的结构如下:

#import <XCTest/XCTest.h>
 
@interface SampleCalcTests : XCTestCase
@end
 
@implementation SampleCalcTests
 
- (void)setUp {
    [super setUp];
    // Put setup code here. This method is called before the invocation of each test method in the class.
}
 
- (void)tearDown {
    // Put teardown code here. This method is called after the invocation of each test method in the class.
    [super tearDown];
}
 
- (void)testExample {
    // This is an example of a functional test case.
    // Use XCTAssert and related functions to verify your tests produce the correct results.
}
 
- (void)testPerformanceExample {
    // This is an example of a performance test case.
    [self measureBlock:^{
        // Put the code you want to measure the time of here.
    }];
}
@end

该实现包含setupteardown实例方法的基本实现,但这些方法并不是必需的。如果所有的测试方法使用了相同的代码,我们可以把这些代码放在setupteardown中。

一般在setup方法中,进行初始化操作、代码复用和准备测试条件。
tearDown方法中,一般会进行释放对象,以避免干扰和回收资源。

测试执行的顺序

默认情况下,当运行测试时,XCTest会查询所有的测试类,并执行每个测试类中所有的测试方法。

对于每个类,都是以setup类方法开始的。对于每个测试方法,都会创建一个新的类的实例并执行setup实例方法。然后执行测试方法,再然后执行teardown实例方法。类中每个测试方法都是如此。当类中最后一个测试方法的teardown方法运行之后,Xcode会执行teardown类方法并移到下一个类中。重复该动作直到所有的测试类都运行完毕。

执行顺序

测试方法的书写

测试方法是一个以test开头,无参无返回的实例方法。如果测试方法中没有达到我们期望的效果,可以使用一组断言来报告失败。

当 Xcode 运行测试时,它会独立调用每个测试方法。因此,每个方法必须准备和清除任何它需要与主题API交互的辅助变量,结构体和对象。如果类中所有的测试方法都有共有的代码,可以把这些代码添加到setuptearDown实例方法中。

下面就是一个单元测试方法:

- (void)testColorIsRed {
   // Set up, call test subject API. (Code could be shared in setUp method.)
   // Test logic and values, assertions report pass/fail to testing framework.
   // Tear down. (Code could be shared in tearDown method.
}

运行测试方法

测试方法的运行有多种类型

run test1
  • 将鼠标放在导航栏上,也就是图中 1的位置,将会运行bundle 中所有的测试方法
  • 将鼠标放在类中的运行按钮处,即图中 2的位置,将运行类中所有的测试方法
  • 将鼠标放在方法右方的运行按钮处,即 图中 3位置处,将只运行该测试方法

或者在类文件中也可以,如下图:


run test 2

测试方法通过将会显示图中的绿色标记,不通过将会出现红色图标,如图所示:


run test fail

如果要运行工程中所有的测试方法,可以选中 Product > Test

运行所有的测试方法

下面的按钮会只显示测试失败的方法

show only fail

异步操作测试

测试是同步执行的,因为每个测试方法都是独立调用的。但越来越多的代码执行是异步的。为了处理调用异步执行方法的组件和函数,XCTest在Xcode 6中进行了加强,包括通过等待异步回调或超时完成之后,在测试方法中序列化异步执行的

文档中提供的例子:

// Test that the document is opened. Because opening is asynchronous,
// use XCTestCase's asynchronous APIs to wait until the document has
// finished opening.
- (void)testDocumentOpening
{
    // Create an expectation object.
    // This test only has one, but it's possible to wait on multiple expectations.

    // 创建一个expectation 对象 这里只创建了一个,但是也可以创建多个expectations

    XCTestExpectation *documentOpenExpectation = [self expectationWithDescription:@"document open"];
 
    NSURL *URL = [[NSBundle bundleForClass:[self class]]
                              URLForResource:@"TestDocument" withExtension:@"mydoc"];
    UIDocument *doc = [[UIDocument alloc] initWithFileURL:URL];
    [doc openWithCompletionHandler:^(BOOL success) {
        XCTAssert(success);
        // Possibly assert other things here about the document after it has opened...
 
        // Fulfill the expectation-this will cause -waitForExpectation
        // to invoke its completion handler and then return.

        // 实现该expectation后会调用  -waitForExpectation 方法的  handler block
        [documentOpenExpectation fulfill];
    }];
 
    // The test will pause here, running the run loop, until the timeout is hit
    // or all expectations are fulfilled.

    // 测试方法将在这暂停, 运行 runloop, 直到 超过`timeout`时间或者所有的 expectation 都实现了
    [self waitForExpectationsWithTimeout:1 handler:^(NSError *error) {
        [doc closeWithCompletionHandler:nil];
    }];
}

上面代码就是在文档打开之后(调用了fulfill方法)或者超过 1s(timeout值)还没打开的话会执行 handler块内的内容。

XCTestExpectation代表了异步测试中的特定条件。- fulfill方法标记该期望被满足。

- (void)waitForExpectationsWithTimeout:(NSTimeInterval)timeout handler:(nullable XCWaitCompletionHandler)handler该方法会在expectation满足或者在timeout时间后执行handler块。

重点:

该方法仅等待 XCTestCase的便利方法创建的expectation。不会等待XCTestExpectation或其子类上的初始化程序手动创建的expectation值。(这里我们创建expectation时的self是指XCTestCase实例,所以会等待)

要等待手动创建的expectation,使用waitForExpectations:timeout:waitForExpectations:timeout:enforceOrder:方法或XCTWaiter上的相应方法,传递一个明确的expectation列表。

性能测试的书写

性能测试会运行想要评估的代码块十次,收集平均执行时间和运行的标准偏差。然后平均值与baseLine进行比较以评估成功或失败。

baseLine是我们指定的用来评估测试通过或者失败的值。我们也可以自己指定一个特定的值。

自定义BaseLine

要实现性能测试,我们可以使用Xcode 6之后的新的API。

- (void)testPerformanceExample {
    // This is an example of a performance test case.
    //  这是一个性能测试的例子
    [self measureBlock:^{
        // Put the code you want to measure the time of here.
        // 把想要测试的代码放在这里
    }];
}

下面的简单示例显示了一个性能测试,用于测试计算器示例应用程序的速度

- (void) testAdditionPerformance {
    [self measureBlock:^{
        // set the initial state
        [calcViewController press:[calcView viewWithTag: 6]];  // 6
        // iterate for 100000 cycles of adding 2
        for (int i=0; i<100000; i++) {
           [calcViewController press:[calcView viewWithTag:13]];  // +
           [calcViewController press:[calcView viewWithTag: 2]];  // 2
           [calcViewController press:[calcView viewWithTag:12]];  // =
        }
    }];
}

UI 测试

UI测试能够查找应用程序的UI并与其进行交互,以验证属性和UI元素的状态。

UI测试包括UI recording,能够生成代码,这些代码可以像用户一样执行应用程序的用户界面,并且可以扩展实现UI测试。这是快速开始编写UI测试的好方法。

UI测试在基本原理方面与单元测试不同。单元测试允许你在app范围内工作,并允许我们通过完全访问应用程序的变量和状态来执行函数和方法。UI测试是以用户相同的方式来操作app的UI,(不会访问app内的方法函数和变量)。这使测试能够以与用户相同的方式查看app,从而暴露用户遇到的UI问题。

APIs

UI测试给予下面三个类的实现:

  • XCUIApplication

  • XCUIElement

  • XCUIElementQuery

UI recording

UI recording 会在测试方法中生成代码,可以对其进行编辑以构建测试或回放特定的使用场景。UI recording 对探索新的UI或学习如何编写UI测试序列也很有用。操作的基本顺序是:

  1. 把鼠标放到方法里

  2. 点击红色录制按钮,开始记录UI。应用程序会启动,这是我们可以点击UI元素(模拟用户行为),鼠标处就会自动生成代码


    UI recording 开始按钮
  3. 完成录制之后,点击停止录制按钮

    停止录制按钮
  4. 可以加入我们自己想要的代码逻辑,比如加入XCTest断言

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

  1. 使用 XCUIElementQuery 查找 XCUIElement
  2. 合成一个事件并把它发送给XCUIElement
  3. 使用断言来比较 XCUIElement 的状态和预期的参考状态

比如,

- (void)testExample {
    // Use recording to get started writing UI tests.
    // Use XCTAssert and related functions to verify your tests produce the correct results.
    
    XCUIApplication *app = [[XCUIApplication alloc] init];
    XCUIElement *loginElement = [app.otherElements containingType:XCUIElementTypeButton identifier:@"login"].element;
    [loginElement twoFingerTap];
    
    XCUIElement *textField = [[app.otherElements containingType:XCUIElementTypeButton identifier:@"login"] childrenMatchingType:XCUIElementTypeTextField].element;
    [textField tap];
    [textField tap];
    [textField tap];
    [textField tap];
    [textField typeText:@"1234"];
    [textField typeText:@"5678"];
    [textField typeText:@"9010"];
    [textField typeText:@"5201"];
}

运行之后:

UI-recording.gif

会自动执行方法里的内容,执行里面的操作(输入文字和点击按钮)

XCTest 断言

断言分为五种类型

  • Unconditional Fail (无条件失败). 当到达特定的代码分支指示失败时使用该断言。这个类型中只有一个断言:XCTFail.

  • Equality Tests. 断言两个item之间的关系。比如,XCTAssertEqual 声明两个表达式具有相同的值,同时XCTAssertEqualWithAccuracy声明两个表达式在一定的准确度内具有相同的值。此类别还包括不等测试,例如XCTAssertNotEqual 和 XCTAssertGreaterThan。

  • Boolean Tests. 断言布尔表达式,比如 XCTAssertTrue 或者 XCTAssertFalse。

  • Nil Tests. 断言一个item是否为nil. 比如 XCTAssertNil 或 XCTAssertNotNil 。

  • Exception Tests (异常测试). 断言一个表达式是否会生成异常,使用 XCTAssertThrows 来抛出异常,也可以使用XCTAssertThrowsSpecific 指定特定的异常,也可以使用 XCTAssertNoThrow 来断言表达式没有异常。

代码覆盖率

代码覆盖率是Xcode 7中的新特性,允许我们可视化并测量代码的执行的程度。通过代码覆盖率,可以确定测试是否正在我们想要的工作。

启用代码覆盖

代码覆盖是LLVM支持的测试选项。当启用代码覆盖,LLVM会根据方法和函数调用的频率来检测代码以收集覆盖率数据。代码覆盖选项可以收集数据以报告测试的正确性和性能,无论是单元测试还是UI测试。

  1. 选中 edit scheme选项(除了下图的方式,还可以通过Product -> Scheme -> Edit Scheme
    F535F6A5-D7A0-4D09-B4DA-DC069941611A.png
  1. 选中Test -> Options -> Code Converage选项
D9BCB122-3BBA-4723-AD87-CE1908F88BEB.png

注意: 代码覆盖数据收集会导致性能损失。 无论该损失是否重要,它都会以线性方式影响代码的执行,所以当启用时,性能结果在测试运行之间保持可比性。但是,当你严格评估性能时需要考虑是否启用代码覆盖

代码覆盖如何适用于测试

代码覆盖率是一个衡量测试价值的工具。它回答了以下问题:

  • 当你运行测试的时候实际运行的代码是什么?
  • 有多少测试才足够?
    换句话说,你是否构建了足够多的测试来确保所有的代码都得到了正确性和性能的检查?
  • 代码中的那些部分没有被测试到?

测试运行完成后,Xcode 将获取LLVM覆盖率数据,并使用它创建一个覆盖率报告。该报告展示了测试运行的主要信息、原文件的列表、文件中的函数以及 每个文件的覆盖百分比

按下图所示,可以查看代码覆盖率:


Reports navigator.png

下面是AFNetworking 的代码覆盖率截图


AFNetworking 的代码覆盖率

点击现实的按钮或者双击,可以跳转到源代码

跳转到源代码

比如我们跳转到了AFAutoPurgingImageCacheTests文件的testThatImagesArePurgedWhenCapcityIsReached方法。右侧显示的是覆盖范围的注释,显示了测试过程中特定代码的执行次数。

执行次数

比如上图的 最上方的 1 指的是该方法执行了一次, 数字11 指的是while循环执行了11次,其他数字同理。

同样的,如果某个方法没有被执行,它的数字就是 0了,

0次执行

(这里我是单独运行了testThatImagesArePurgedWhenCapcityIsReached方法,所以上图所示的方法当然是运行0次了)

最后

Xcode对测试的集成支持使您能够编写测试以各种方式支持您的开发工作。您我们可以使用测试来检测代码中的潜在问题,发现是否符合预期,并验证应用程序的行为,提高代码的稳定性。

当然,通过测试获得的稳定水平取决于编写的测试代码的质量。同样,编写好测试的难易程度取决于编写代码的方式。阅读以下指导原则以确保您的代码是可测试的,并且可以简化编写良好测试的过程。

  • 定义API的要求. 定义添加到项目中的每种方法或功能的需求和结果非常重要。对于需求,包括输入和输出范围,抛出的异常和引发它们的条件以及返回值的类型。指定需求并确定代码中需求已经达到可以使我们的代码更加安全强壮。

  • 编写代码时编写测试用例. 在设计和编写每个方法和函数时,编写一个或多个测试用例以确保符合API的需求。为现有代码编写测试比在编写代码时就写测试更困难

  • 检查边界条件. 如果方法的参数必须具有特定范围内的值,则测试应传递包含范围的最低和最高值的值。

  • 使用negative test(负面测试). negative test 可确保我们的代码适当地响应错误条件。验证我们的代码在接收到无效或意外的输入值时行为正确。还应验证它是否返回错误代码或引发异常。例如,如果一个整数参数必须是在0到100范围内,测试用例值传递-1和101以确保该过程引发一个异常或返回错误代码。


大家也可以查看苹果的 官方文档

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容