写好测试,提高应用质量

相信在国内一些中小型公司,开发者不多会去写软件测试相关的代码。固然这背后有一些缘由在。本文就讲讲 iOS 开发中的软件测试相关的内容。

1、 测试的重要性

测试很重要!测试很重要!测试很重要!重要的事情说三遍。c++

场景1:每次咱们写完代码后都须要编译运行,以查看应用程序的表现是否符合预期。假如改动点、代码量小,那验证成本低一些,假如不符合预期,则说明咱们的代码有问,人工去排查问题花费的时间也少一些。假如改动点不少、受影响的地方较多,咱们首先要大概猜想受影响的功能,而后去定位问题、排查问题的成本就很高。git

场景2:你新接手的 SDK 某个子功能须要作一次技术重构。可是你只有在公司内部的代码托管平台上能够看到一些 Readme、接入文档、系统设计文档、技术方案评估文档等一堆文档。可能你会看完再去动手重构。当你重构完了,找了公司某条业务线的 App 接入测试,点了几下发现发生了奔溃。😂 心想,本地测试、debug 都正常但是为何接入后就 Crash 了。其实想一想也好理解,你本地重构只是确保了你开发的那个功能运行正常,你很难确保你写的代码没有影响其余类、其余功能。假如以前的 SDK 针对每一个类都有单元测试代码,那你在新功能开发完毕后完整跑一次单元测试代码就行了,保证每一个 Unit Test 都经过、分支覆盖率达到约定的线,那么基本上是没问题的。github

场景3:在版本迭代的时候,计划功能 A,从开发、联调、测试、上线共2周时间。老司机作事很自信,这么简单的 UI、动画、交互,代码风骚,参考服务端的「领域驱动」在该 feature 开发阶段落地试验了下。联调、本地测试都经过了,还剩3天时间,本觉得测试1天,bug fix 一天,最后一天提交审核。代码跟你开了个玩笑,测试完 n 个 bug(大大超出预期)。为了避免影响 App 的发布上架,不得不熬夜修 bug。将全部的测试都经过测试工程师去处理,这个阶段理论上质量应该很稳定,否则该阶段发现代码异常、技术设计有漏洞就来不及了,你须要协调各个团队的资源(可能接口要改动、产品侧要改动),这个阶段形成改动的成本很是大。objective-c

相信大多数开发者都遇到过上述场景的问题。其实上述这几个问题都有解,那就是“软件测试”。数据库

2、软件测试

1. 分类

软件测试就是在规定的条件下对应用程序进行操做,以发现程序错误,衡量软件质量,并对其是否能知足设计要求进行评估的过程。express

合理应用软件测试技术,就能够规避掉第一部分的3个场景下的问题。编程

软件测试强调开发、测试同步进行,甚至是测试先行,从需求评审阶段就先考虑好软件测试方案,随后才进行技术方案评审、开发编码、单元测试、集成测试、系统测试、回归测试、验收测试等。json

软件测试从测试范围分为:单元测试、集成测试、系统测试、回归测试、验收测试(有些公司会谈到“冒烟测试“,这个词的精肯定义不知道,可是学软件测试课的时候按照范围就只有上述几个分类)。工程师本身负责的是单元测试。测试工程师、QA 负责的是集成测试、系统测试。安全

单元测试(Unit Testing):又称为模块测试,是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工做。「单元」的概念会比较抽象,它不只仅是咱们所编写的某个方法、函数,也多是某个类、对象等。网络

软件测试从开发模式分为:面向测试驱动开发 TDD (Test-driven development)、面向行为驱动开发 BDD (Behavior-driven development)。

2. TDD

TDD 的思想是:先编写测试用例,再快速开发代码,而后在测试用例的保证下,能够方便安全地进行代码重构,提高应用程序的质量。一言以蔽之就是经过测试来推进开发的进行。正是因为这个特色,TDD 被普遍使用于敏捷开发。

也就是说 TDD 模式下,首先考虑如何针对功能进行测试,而后去编写代码实现,再不断迭代,在测试用例的保证下,不断进行代码优化。

优势:目标明确、架构分层清晰。可保证开发代码不会偏离需求。每一个阶段持续测试

缺点:技术方案须要先评审结束、架构须要提早搭建好。假如需求变更,则前面步骤须要从新执行,灵活性较差。

3. BDD

BDD 即行为驱动开发,是敏捷开发技术之一,经过天然语言定义系统行为,以功能使用者的角度,编写需求场景,且这些行为描述能够直接造成需求文档,同时也是测试标准。

BDD 的思想是跳出单一的函数,针对的是行为而展开的测试。BDD 关心的是业务领域、行为方式,而不是具体的函数、方法,经过对行为的描述来验证功能的可用性。BDD 使用 DSL (Domin Specific Language)领域特定语言来描述测试用例,这样编写的测试用例很是易读,看起来跟文档同样易读,BDD 的代码结构是 Given->When->Then

优势:各团队的成员能够集中在一块儿,设计基于行为的计测试用例。

4. 对比

根据特色也就是找到了各自的使用场景,TDD 主要针对开发中的最小单元进行测试,适合单元测试。而 BDD 针对的是行为,因此测试范围能够再大一些,在集成测试、系统测试中均可以使用

TDD 编写的测试用例通常针对的是开发中的最小单元(好比某个类、函数、方法)而展开,适合单元测试。

BDD 编写的测试用例针对的是行为,测试范围更大一些,适合集成测试、系统测试阶段。

3、 单元测试编码规范

本文的主要重点是针对平常开发阶段工程师能够作的事情,也就是单元测试而展开。

编写功能、业务代码的时候通常会遵循 kiss 原则 ,因此类、方法、函数每每不会太大,分层设计越好、职责越单1、耦合度越低的代码越适合作单元测试,单元测试也倒逼开发过程当中代码分层、解耦。

可能某个功能的实现代码有30行,测试代码有50行。单元测试的代码如何编写才更合理、整洁、规范呢?

1. 编码分模块展开

先贴一段代码。

-  (void)testInsertDataInOneSpecifiedTable
{
    XCTestExpectation *exception = [self expectationWithDescription:@"测试数据库插入功能"];
    // given
    [dbInstance removeAllLogsInTableType:HCTLogTableTypeMeta];
    NSMutableArray *insertModels = [NSMutableArray array];
    for (NSInteger index = 1; index <= 10000; index++) {
        HCTLogMetaModel *model = [[HCTLogMetaModel alloc] init];
        model.log_id = index;
                // ...
        [insertModels addObject:model];
    }
    // when
    [dbInstance add:insertModels inTableType:HCTLogTableTypeMeta];
       // then 
      [dbInstance recordsCountInTableType:HCTLogTableTypeMeta completion:^(NSInteger count) {
        XCTAssert(count == insertModels.count, @"「数据增长」功能:异常");
        [exception fulfill];
    }];
    [self waitForExpectationsWithCommonTimeout];
}

能够看到这个方法的名称为 testInsertDataInOneSpecifiedTable,这段代码作的事情经过函数名能够看出来:测试插入数据到某个特定的表。这个测试用例分为3部分:测试环境所需的先决条件准备;调用所要测试的某个方法、函数;验证输出和行为是否符合预期。

其实,每一个测试用例的编写也要按照该种方式去组织代码。步骤分为3个阶段:Given->When->Then。

因此单元测试的代码规范也就出来了。此外单元测试代码规范统一后,每一个人的测试代码都按照这个标准展开,那其余人的阅读起来就更加容易、方便。按照这3个步骤去阅读、理解测试代码,就能够清晰明了的知道在作什么。

2. 一个测试用例只测试一个分支

咱们写的代码有不少语句组成,有各类逻辑判断、分支(if...else、swicth)等等,所以一个程序从一个单一入口进去,过程可能产生 n 个不一样的分支,可是程序的出口老是一个。因此因为这样的特性,咱们的测试也须要针对这样的现状走完尽量多的分支。相应的指标叫作「分支覆盖率」。

假如某个方法内部有 if...else...,咱们在测试的时候尽可能将每种状况写成一个单独的测试用例,单独的输入、输出,判断是否符合预期。这样每一个 case 都单一的测试某个分支,可读性也很高。

好比对下面的函数作单元测试,测试用例设计以下

- (void)shouldIEatSomething
{
   BOOL shouldEat = [self getAteWeight] < self.dailyFoodSupport;
   if (shouldEat) {
     [self eatSomemuchFood];
   } else {
     [self doSomeExercise];
   }
}
- (void)testShouldIEatSomethingWhenHungry
{
   // ....
}

- (void)testShouldIEatSomethingWhenFull
{
  // ...
}

3. 明确标识被测试类

这条主要站在团队合做和代码可读性角度出发来讲明。写过单元测试的人都知道,可能某个函数原本就10行代码,但是为了测试它,测试代码写了30行。一个方法这样写问题不大,多看看就看明白是在测试哪一个类的哪一个方法。但是当这个类自己就很大,测试代码很大的状况下,无论是做者自身仍是多年后负责维护的其余同事,看这个代码阅读成本会很大,须要先看测试文件名 代码类名 + Test 才知道是测试的是哪一个类,看测试方法名 test + 方法名 才知道是测试的是哪一个方法。

这样的代码可读性不好,因此应该为当前的测试对象特殊标记,这样测试代码可读性越强、阅读成本越低。好比定义局部变量 _sut 用来标记当前被测试类(sut,System under Test,软件测试领域有个词叫作被测系统,用来表示正在被测试的系统)。

#import <XCTest/XCTest.h>
#import "HCTLogPayloadModel.h"

@interface HCTLogPayloadModelTest : HCTTestCase
{
    HCTLogPayloadModel *_sut;
}

@end

@implementation HCTLogPayloadModelTest

- (void)setUp
{
    [super setUp];
    HCTLogPayloadModel *model = [[HCTLogPayloadModel alloc] init];
    model.log_id = 1;
    // ...
    _sut = model;
}

- (void)tearDown
{
    _sut = nil;
    [super tearDown];
}

- (void)testGetDictionary
{
    NSDictionary *payloadDictionary = [_sut getDictionary];
    XCTAssert([(NSString *)payloadDictionary[@"report_id"] isEqualToString:@"001"] &&
              [payloadDictionary[@"size"] integerValue] == 102 &&
              [(NSString *)payloadDictionary[@"meta"] containsString:@"meiying"],
              @"HCTLogPayloadModel 的 「getDictionary」功能异常");
}

@end

4. 使用分类来暴露私有方法、私有变量

某些场景下写的测试方法内部可能须要调用被测对象的私有方法,也可能须要访问被测对象的某个私有属性。可是测试类里面是访问不到被测类的私有属性和私有方法的,借助于 Category 能够实现这样的需求。

为测试类添加一个分类,后缀名为 UnitTest。以下所示

HermesClient 类有私有属性 @property (nonatomic, strong) NSString *name;,私有方法 - (void)hello。为了在测试用例中访问私有属性和私有方法,写了以下分类

// HermesClientTest.m

@interface HermesClient (UnitTest)

- (NSString *)name;

- (void)hello;

@end
  
@implementation HermesClientTest

- (void)testPrivatePropertyAndMethod
{
    NSLog(@"%@",[HermesClient sharedInstance].name);
    [[HermesClient sharedInstance] hello];
}
@end

4、 单元测试下开发模式、技术框架选择

单元测试是按照测试范围来划分的。TDD、BDD 是按照开发模式来划分的。所以就有各类排列组合,这里咱们只关心单元测试下的 TDD、BDD 方案。

在单元测试阶段,TDD 和 BDD 均可以适用。

1. TDD

TDD 强调不断的测试推进代码的开发,这样简化了代码,保证了代码质量。

思想是在拿到一个新的功能时,首先思考该功能如何测试,各类测试用例、各类边界 case;而后完成测试代码的开发;最后编写相应的代码以知足、经过这些测试用例。

TDD 开发过程相似下图:

  • 先编写该功能的测试用例,实现测试代码。这时候去跑测试,是不经过的,也就是到了红色的状态
  • 而后编写真正的功能实现代码。这时候去跑测试,测试经过,也就是到了绿色的状态
  • 在测试用例的保证下,能够重构、优化代码

抛出一个问题:TDD 看上去很好,应该用它吗?

这个问题不用着急回答,回答了也不会有对错之分。开发中常常是这样一个流程,新的需求出来后,先通过技术评审会议,肯定宏观层面的技术方案、肯定各个端的技术实现、使用的技术等,整理出开发文档、会议文档。工期评估后开始编码。事情这么简单吗?前期即便想的再充分、再细致,可能仍是存在特殊 case 漏掉的状况,致使技术方案或者是技术实现的改变。若是采用 TDD,那么以前新功能给到后,就要考虑测试用例的设计、编写了测试代码,在测试用例的保证下再去实现功能。若是遇到了技术方案的变动,以前的测试用例要改变、测试代码实现要改变。可能新增的某个 case 致使大部分的测试代码和实现代码都要改变。

如何开展 TDD**

  1. 新建一个工程,确保 “Include Unit Tests” 选项是选中的状态

    TDD Step 1

  2. 建立后的工程目录以下

    TDD step2

  3. 删除 Xcode 建立的测试模版文件 TDDDemoTests.m
  4. 假如咱们须要设计一我的类,它具备吃饭的功能,且当他吃完后会说一句“好饱啊”。
  5. 那么按照 TDD 咱们先设计测试用例。假设有个 Person 类,有个对象方法叫作吃饭,吃完饭后会返回一个“好饱啊”的字符串。那测试用例就是

    步骤 指望 结果
    实例化 Person 对象,调用对象的 eat 方法 调用后返回“好饱啊”
  6. 实现测试用例代码。建立继承自 Unit Test Case class 的测试类,命名为 工程前缀+测试类名+Test,也就是 TDDPersonTest.m

    TDD step 3

  7. 由于要测试 Person 类,因此在主工程中建立 Person 类
  8. 由于要测试人类在吃饭后说一句“好饱啊”。因此设想那个类目前只有一个吃饭的方法。因而在 TDDPersonTest.m 中建立一个测试函数 -(void)testReturnStatusStringWhenPersonAte;函数内容以下

    - (void)testReturnStatusStringWhenPersonAte
    {
        // Given
        Person *somebody = [[Person alloc] init];
        
        // When
        NSString *statusMessage = [somebody performSelector:@selector(eat)];
        
        // Then
        XCTAssert([statusMessage isEqualToString:@"好饱啊"], @"Person 「吃饭后返回“好饱啊”」功能异常");
    }
  9. Xcode 下按快捷键 Command + U,跑测试代码发现是失败的。由于咱们的 Person 类根本没实现相应的方法
  10. TDD 开发过程能够看到,咱们如今是红色的 “Fail” 状态。因此须要去 Person 类中实现功能代码。Person 类以下

    #import "Person.h"
    
    @implementation Person
    
    - (NSString *)eat
    {
        [NSThread sleepForTimeInterval:1];
        return @"好饱啊";;
    }
    
    @end
  11. 再次运行,跑一下测试用例(Command + U 快捷键)。发现测试经过,也就是TDD 开发过程中的绿色 “Success” 状态。
  12. 例子比较简单,假如状况须要,能够在 -(void)setUp 方法里面作一些测试的前置准备工做,在 -(void)tearDown 方法里作资源释放的操做
  13. 假如 eat 方法实现的不够漂亮。如今在测试用例的保证下,大胆重构,最后确保全部的 Unit Test case 经过便可。

2. BDD

相比 TDD,BDD 关注的是行为方式的设计,拿上述“人吃饭”举例说明。

和 TDD 相比第1~4步骤相同。

  1. BDD 则须要先实现功能代码。建立 Person 类,实现 -(void)eat;方法。代码和上面的相同
  2. BDD 须要引入好用的框架 Kiwi,使用 Pod 的方式引入
  3. 由于要测试人类在吃饭后说一句“好饱啊”。因此设想那个类目前只有一个吃饭的方法。因而在 TDDPersonTest.m 中建立一个测试函数 -(void)testReturnStatusStringWhenPersonAte;函数内容以下

    #import "kiwi.h"
    #import "Person.h"
    
    SPEC_BEGIN(BDDPersonTest)
    
    describe(@"Person", ^{
        context(@"when someone ate", ^{
            it(@"should get a string",^{
                  Person *someone = [[Person alloc] init];
                NSString *statusMessage = [someone eat];
                [[statusMessage shouldNot] beNil];
                [[statusMessage should] equal:@"好饱啊"];
            });
        });
    });
    
    SPEC_END

3. XCTest

开发步骤

Xcode 自带的测试系统是 XCTest,使用简单。开发步骤以下

  • Tests 目录下为被测的类建立一个继承自 XCTestCase 的测试类。
  • 删除新建的测试代码模版里面的无用方法 - (void)testPerformanceExample- (void)testExample
  • 跟普通类同样,能够继承,能够写私有属性、私有方法。因此能够在新建的类里面,根据需求写一些私有属性等
  • - (void)setUp 方法里面写一些初始化、启动设置相关的代码。好比测试数据库功能的时候,写一些数据库链接池相关代码
  • 为被测类里面的每一个方法写测试方法。被测类里面多是 n 个方法,测试类里面多是 m 个方法(m >= n),根据咱们在第三部分:单元测试编码规范里讲过的 一个测试用例只测试一个分支,方法内部有 if、switch 语句时,须要为每一个分支写测试用例
  • 为测试类每一个方法写的测试方法有必定的规范。命名必须是 test+被测方法名。函数无参数、无返回值。好比 - (void)testSharedInstance
  • 测试方法里面的代码按照 Given->When->Then 的顺序展开。测试环境所需的先决条件准备;调用所要测试的某个方法、函数;使用断言验证输出和行为是否符合预期。
  • - (void)tearDown 方法里面写一些释放掉资源或者关闭的代码。好比测试数据库功能的时候,写一些数据库链接池关闭的代码

断言相关宏

/*!
 * @function XCTFail(...)
 * Generates a failure unconditionally.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTFail(...) \
    _XCTPrimitiveFail(self, __VA_ARGS__)

/*!
 * @define XCTAssertNil(expression, ...)
 * Generates a failure when ((\a expression) != nil).
 * @param expression An expression of id type.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertNil(expression, ...) \
    _XCTPrimitiveAssertNil(self, expression, @#expression, __VA_ARGS__)

/*!
 * @define XCTAssertNotNil(expression, ...)
 * Generates a failure when ((\a expression) == nil).
 * @param expression An expression of id type.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertNotNil(expression, ...) \
    _XCTPrimitiveAssertNotNil(self, expression, @#expression, __VA_ARGS__)

/*!
 * @define XCTAssert(expression, ...)
 * Generates a failure when ((\a expression) == false).
 * @param expression An expression of boolean type.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssert(expression, ...) \
    _XCTPrimitiveAssertTrue(self, expression, @#expression, __VA_ARGS__)

/*!
 * @define XCTAssertTrue(expression, ...)
 * Generates a failure when ((\a expression) == false).
 * @param expression An expression of boolean type.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertTrue(expression, ...) \
    _XCTPrimitiveAssertTrue(self, expression, @#expression, __VA_ARGS__)

/*!
 * @define XCTAssertFalse(expression, ...)
 * Generates a failure when ((\a expression) != false).
 * @param expression An expression of boolean type.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertFalse(expression, ...) \
    _XCTPrimitiveAssertFalse(self, expression, @#expression, __VA_ARGS__)

/*!
 * @define XCTAssertEqualObjects(expression1, expression2, ...)
 * Generates a failure when ((\a expression1) not equal to (\a expression2)).
 * @param expression1 An expression of id type.
 * @param expression2 An expression of id type.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertEqualObjects(expression1, expression2, ...) \
    _XCTPrimitiveAssertEqualObjects(self, expression1, @#expression1, expression2, @#expression2, __VA_ARGS__)

/*!
 * @define XCTAssertNotEqualObjects(expression1, expression2, ...)
 * Generates a failure when ((\a expression1) equal to (\a expression2)).
 * @param expression1 An expression of id type.
 * @param expression2 An expression of id type.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertNotEqualObjects(expression1, expression2, ...) \
    _XCTPrimitiveAssertNotEqualObjects(self, expression1, @#expression1, expression2, @#expression2, __VA_ARGS__)

/*!
 * @define XCTAssertEqual(expression1, expression2, ...)
 * Generates a failure when ((\a expression1) != (\a expression2)).
 * @param expression1 An expression of C scalar type.
 * @param expression2 An expression of C scalar type.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertEqual(expression1, expression2, ...) \
    _XCTPrimitiveAssertEqual(self, expression1, @#expression1, expression2, @#expression2, __VA_ARGS__)

/*!
 * @define XCTAssertNotEqual(expression1, expression2, ...)
 * Generates a failure when ((\a expression1) == (\a expression2)).
 * @param expression1 An expression of C scalar type.
 * @param expression2 An expression of C scalar type.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertNotEqual(expression1, expression2, ...) \
    _XCTPrimitiveAssertNotEqual(self, expression1, @#expression1, expression2, @#expression2, __VA_ARGS__)

/*!
 * @define XCTAssertEqualWithAccuracy(expression1, expression2, accuracy, ...)
 * Generates a failure when (difference between (\a expression1) and (\a expression2) is > (\a accuracy))).
 * @param expression1 An expression of C scalar type.
 * @param expression2 An expression of C scalar type.
 * @param accuracy An expression of C scalar type describing the maximum difference between \a expression1 and \a expression2 for these values to be considered equal.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertEqualWithAccuracy(expression1, expression2, accuracy, ...) \
    _XCTPrimitiveAssertEqualWithAccuracy(self, expression1, @#expression1, expression2, @#expression2, accuracy, @#accuracy, __VA_ARGS__)

/*!
 * @define XCTAssertNotEqualWithAccuracy(expression1, expression2, accuracy, ...)
 * Generates a failure when (difference between (\a expression1) and (\a expression2) is <= (\a accuracy)).
 * @param expression1 An expression of C scalar type.
 * @param expression2 An expression of C scalar type.
 * @param accuracy An expression of C scalar type describing the maximum difference between \a expression1 and \a expression2 for these values to be considered equal.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertNotEqualWithAccuracy(expression1, expression2, accuracy, ...) \
    _XCTPrimitiveAssertNotEqualWithAccuracy(self, expression1, @#expression1, expression2, @#expression2, accuracy, @#accuracy, __VA_ARGS__)

/*!
 * @define XCTAssertGreaterThan(expression1, expression2, ...)
 * Generates a failure when ((\a expression1) <= (\a expression2)).
 * @param expression1 An expression of C scalar type.
 * @param expression2 An expression of C scalar type.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertGreaterThan(expression1, expression2, ...) \
    _XCTPrimitiveAssertGreaterThan(self, expression1, @#expression1, expression2, @#expression2, __VA_ARGS__)

/*!
 * @define XCTAssertGreaterThanOrEqual(expression1, expression2, ...)
 * Generates a failure when ((\a expression1) < (\a expression2)).
 * @param expression1 An expression of C scalar type.
 * @param expression2 An expression of C scalar type.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertGreaterThanOrEqual(expression1, expression2, ...) \
    _XCTPrimitiveAssertGreaterThanOrEqual(self, expression1, @#expression1, expression2, @#expression2, __VA_ARGS__)

/*!
 * @define XCTAssertLessThan(expression1, expression2, ...)
 * Generates a failure when ((\a expression1) >= (\a expression2)).
 * @param expression1 An expression of C scalar type.
 * @param expression2 An expression of C scalar type.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertLessThan(expression1, expression2, ...) \
    _XCTPrimitiveAssertLessThan(self, expression1, @#expression1, expression2, @#expression2, __VA_ARGS__)

/*!
 * @define XCTAssertLessThanOrEqual(expression1, expression2, ...)
 * Generates a failure when ((\a expression1) > (\a expression2)).
 * @param expression1 An expression of C scalar type.
 * @param expression2 An expression of C scalar type.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertLessThanOrEqual(expression1, expression2, ...) \
    _XCTPrimitiveAssertLessThanOrEqual(self, expression1, @#expression1, expression2, @#expression2, __VA_ARGS__)

/*!
 * @define XCTAssertThrows(expression, ...)
 * Generates a failure when ((\a expression) does not throw).
 * @param expression An expression.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertThrows(expression, ...) \
    _XCTPrimitiveAssertThrows(self, expression, @#expression, __VA_ARGS__)

/*!
 * @define XCTAssertThrowsSpecific(expression, exception_class, ...)
 * Generates a failure when ((\a expression) does not throw \a exception_class).
 * @param expression An expression.
 * @param exception_class The class of the exception. Must be NSException, or a subclass of NSException.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertThrowsSpecific(expression, exception_class, ...) \
    _XCTPrimitiveAssertThrowsSpecific(self, expression, @#expression, exception_class, __VA_ARGS__)

/*!
 * @define XCTAssertThrowsSpecificNamed(expression, exception_class, exception_name, ...)
 * Generates a failure when ((\a expression) does not throw \a exception_class with \a exception_name).
 * @param expression An expression.
 * @param exception_class The class of the exception. Must be NSException, or a subclass of NSException.
 * @param exception_name The name of the exception.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertThrowsSpecificNamed(expression, exception_class, exception_name, ...) \
    _XCTPrimitiveAssertThrowsSpecificNamed(self, expression, @#expression, exception_class, exception_name, __VA_ARGS__)

/*!
 * @define XCTAssertNoThrow(expression, ...)
 * Generates a failure when ((\a expression) throws).
 * @param expression An expression.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertNoThrow(expression, ...) \
    _XCTPrimitiveAssertNoThrow(self, expression, @#expression, __VA_ARGS__)

/*!
 * @define XCTAssertNoThrowSpecific(expression, exception_class, ...)
 * Generates a failure when ((\a expression) throws \a exception_class).
 * @param expression An expression.
 * @param exception_class The class of the exception. Must be NSException, or a subclass of NSException.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertNoThrowSpecific(expression, exception_class, ...) \
    _XCTPrimitiveAssertNoThrowSpecific(self, expression, @#expression, exception_class, __VA_ARGS__)

/*!
 * @define XCTAssertNoThrowSpecificNamed(expression, exception_class, exception_name, ...)
 * Generates a failure when ((\a expression) throws \a exception_class with \a exception_name).
 * @param expression An expression.
 * @param exception_class The class of the exception. Must be NSException, or a subclass of NSException.
 * @param exception_name The name of the exception.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertNoThrowSpecificNamed(expression, exception_class, exception_name, ...) \
    _XCTPrimitiveAssertNoThrowSpecificNamed(self, expression, @#expression, exception_class, exception_name, __VA_ARGS__)

经验小结

  1. XCTestCase 类和其余类同样,你能够定义基类,这里面封装一些经常使用的方法。

    // HCTTestCase.h
    #import <XCTest/XCTest.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface HCTTestCase : XCTestCase
    
    @property (nonatomic, assign) NSTimeInterval networkTimeout;
    
    
    /**
     用一个默认时间设置异步测试 XCTestExpectation 的超时处理
     */
    - (void)waitForExpectationsWithCommonTimeout;
    
    /**
     用一个默认时间设置异步测试的
    
     @param handler 超时的处理逻辑
     */
    - (void)waitForExpectationsWithCommonTimeoutUsingHandler:(XCWaitCompletionHandler __nullable)handler;
    
    
    /**
     生成 Crash 类型的 meta 数据
    
     @return meta 类型的字典
     */
    - (NSDictionary *)generateCrashMetaDataFromReport;
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    // HCTTestCase.m
    #import "HCTTestCase.h"
    #import ...
    
    @implementation HCTTestCase
    
    #pragma mark - life cycle
    
    - (void)setUp
    {
        [super setUp];
        self.networkTimeout = 20.0;
        // 1. 设置平台信息
        [self setupAppProfile];
        // 2. 设置 Mget 配置
        [[TITrinityInitManager sharedInstance] setup];
        // ....
        // 3. 设置 HermesClient
        [[HermesClient sharedInstance] setup];
    }
    
    - (void)tearDown
    {
        [super tearDown];
    }
    
    
    #pragma mark - public Method
    
    - (void)waitForExpectationsWithCommonTimeout
    {
        [self waitForExpectationsWithCommonTimeoutUsingHandler:nil];
    }
    
    - (void)waitForExpectationsWithCommonTimeoutUsingHandler:(XCWaitCompletionHandler __nullable)handler
    {
        [self waitForExpectationsWithTimeout:self.networkTimeout handler:handler];
    }
    
    
    - (NSDictionary *)generateCrashMetaDataFromReport
    {
        NSMutableDictionary *metaDictionary = [NSMutableDictionary dictionary];
        NSDate *crashTime = [NSDate date];
        metaDictionary[@"MONITOR_TYPE"] = @"appCrash";
        // ...
        metaDictionary[@"USER_CRASH_DATE"] = @([crashTime timeIntervalSince1970] * 1000);
        return [metaDictionary copy];
    }
    
    
    #pragma mark - private method
    
    - (void)setupAppProfile
    {
        [[CMAppProfile sharedInstance] setMPlatform:@"70"];
        // ... 
    }
    
    @end
  2. 上述说的基本是开发规范相关。测试方法内部若是调用了其余类的方法,则在测试方法内部必须 Mock 一个外部对象,限制好返回值等。
  3. 在 XCTest 内难以使用 mock 或 stub,这些是测试中很是常见且重要的功能

例子

这里举个例子,是测试一个数据库操做类 HCTDatabase,代码只放某个方法的测试代码。

- (void)testRemoveLatestRecordsByCount
{
    XCTestExpectation *exception = [self expectationWithDescription:@"测试数据库删除最新数据功能"];
    // 1. 先清空数据表
    [dbInstance removeAllLogsInTableType:HCTLogTableTypeMeta];
    // 2. 再插入一批数据
    NSMutableArray *insertModels = [NSMutableArray array];
    NSMutableArray *reportIDS = [NSMutableArray array];
    
    for (NSInteger index = 1; index <= 100; index++) {
        HCTLogMetaModel *model = [[HCTLogMetaModel alloc] init];
        model.log_id = index;
        // ...
        if (index > 90 && index <= 100) {
            [reportIDS addObject:model.report_id];
        }
        [insertModels addObject:model];
    }
    [dbInstance add:insertModels inTableType:HCTLogTableTypeMeta];
    
    // 3. 将早期的数据删除掉(id > 90 && id <= 100)
    [dbInstance removeLatestRecordsByCount:10 inTableType:HCTLogTableTypeMeta];
    
    // 4. 拿到当前的前10条数据和以前存起来的前10条 id 作比较。再判断当前表中的总记录条数是否等于 90
    [dbInstance getLatestRecoreds:10 inTableType:HCTLogTableTypeMeta completion:^(NSArray<HCTLogModel *> * _Nonnull records) {
        NSArray<HCTLogModel *> *latestRTentRecords = records;
        
        [dbInstance getOldestRecoreds:100 inTableType:HCTLogTableTypeMeta completion:^(NSArray<HCTLogModel *> * _Nonnull records) {
            NSArray<HCTLogModel *> *currentRecords = records;
            
            __block BOOL isEarlyData = NO;
            [latestRTentRecords enumerateObjectsUsingBlock:^(HCTLogModel * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
                if ([reportIDS containsObject:obj.report_id]) {
                    isEarlyData = YES;
                }
            }];
            
            XCTAssert(!isEarlyData && currentRecords.count == 90, @"***Database「删除最新n条数据」功能:异常");
            [exception fulfill];
        }];
        
    }];
    [self waitForExpectationsWithCommonTimeout];
}

3. 测试框架

1. Kiwi

BDD 框架里的 Kiwi 可圈可点。使用 CocoaPods 引入 pod 'Kiwi'。看下面的例子

被测类(Planck 项目是一个基于 WebView 的 SDK,根据业务场景,发现针对 WebView 的大部分功能定制都是基于 WebView 的生命周期内发生的,因此参考 NodeJS 的中间件思想,设计了基于生命周期的 WebView 中间件)

#import <Foundation/Foundation.h>

@interface TPKTrustListHelper : NSObject

+(void)fetchRemoteTrustList;

+(BOOL)isHostInTrustlist:(NSString *)scheme;

+(NSArray *)trustList;

@end

测试类

SPEC_BEGIN(TPKTrustListHelperTest)
describe(@"Middleware Wrapper", ^{
    
    context(@"when get trustlist", ^{
        it(@"should get a array of string",^{
            NSArray *array = [TPKTrustListHelper trustList];
            [[array shouldNot] beNil];
            NSString *first = [array firstObject];
            [[first shouldNot] beNil];
            [[NSStringFromClass([first class]) should] equal:@"__NSCFString"];
        });
    });
    
    context(@"when check a string wether contained in trustlist ", ^{
        it(@"first string should contained in trustlist",^{
            NSArray *array = [TPKTrustListHelper trustList];
            NSString *first = [array firstObject];
            [[theValue([TPKTrustListHelper isHostInTrustlist:first]) should] equal:@(YES)];
        });
    });
});
SPEC_END

例子包含 Kiwi 的最基础元素。SPEC_BEGINSPEC_END 表示测试类;describe 描述须要被测试的类;context 表示一个测试场景,也就是 Given->When->Then 里的 Givenit 表示要测试的内容,也就是也就是 Given->When->Then 里的 WhenThen。1个 describe 下能够包含多个 context,1个 context 下能够包含多个 it

Kiwi 的使用分为:SpecsExpectationsMocks and StubsAsynchronous Testing 四部分。点击能够访问详细的说明文档。

it 里面的代码块是真正的测试代码,使用链式调用的方式,简单上手。

测试领域中 Mock 和 Stub 很是重要。Mock 模拟对象能够下降对象之间的依赖,模拟出一个纯净的测试环境(相似初中物理课上“控制变量法”的思想)。Kiwi 也支持的很是好,能够模拟对象、模拟空对象、模拟遵循协议的对象等等,点击 Mocks and Stubs 查看。Stub 存根能够控制某个方法的返回值,这对于方法内调用别的对象的方法返回值颇有帮助。减小对于外部的依赖,单一测试当前行为是否符合预期。

针对异步测试,XCTest 则须要建立一个 XCTestExpectation 对象,在异步实现里面调用该对象的 fulfill 方法,最后设置最大等待时间和完成的回调 - (void)waitForExpectationsWithTimeout:(NSTimeInterval)timeout handler:(nullable XCWaitCompletionHandler)handler; 以下例子

XCTestExpectation *exception = [self expectationWithDescription:@"测试数据库插入功能"];
    [dbInstance removeAllLogsInTableType:HCTLogTableTypeMeta];
    NSMutableArray *insertModels = [NSMutableArray array];
    for (NSInteger index = 1; index <= 10000; index++) {
        HCTLogMetaModel *model = [[HCTLogMetaModel alloc] init];
        model.log_id = index;
          // 。。。
        [insertModels addObject:model];
    }
    [dbInstance add:insertModels inTableType:HCTLogTableTypeMeta];
    [dbInstance recordsCountInTableType:HCTLogTableTypeMeta completion:^(NSInteger count) {
        XCTAssert(count == insertModels.count, @"**Database「数据增长」功能:异常");
        [exception fulfill];
    }];
    [self waitForExpectationsWithCommonTimeout];

2. expecta、Specta

expecta 和 Specta 都出自 orta 之手,他也是 Cocoapods 的开发者之一。太牛逼了,工程化、质量保证领域的大佬。

Specta 是一个轻量级的 BDD 测试框架,采用 DSL 模式,让测试更接近于天然语言,所以更易读。

特色:

  • 易于集成到项目中。在 Xcode 中勾选 Include Unit Tests ,和 XCTest 搭配使用
  • 语法很规范,对比 Kiwi 和 Specta 的文档,发现不少东西都是相同的,也就是很规范,因此学习成本低、后期迁移到其余框架很平滑。

Expecta 是一个匹配(断言)框架,相比 Xcode 的断言 XCAssert,Excepta 提供更加丰富的断言。

特色:

  • Eepecta 没有数据类型限制,好比 1,并不关心是 NSInteger 仍是 CGFloat
  • 链式编程,写起来很舒服
  • 反向匹配,很灵活。断言匹配用 except(...).to.equal(...),断言不匹配则使用 .notTo 或者 .toNot
  • 延时匹配,能够在链式表达式后加入 .will.willNot.after(interval)

4. 小结

Xcode 自带的 XCTestCase 比较适合 TDD,不影响源代码,系统独立且不影响 App 包大小。适合简单场景下的测试。且每一个函数在最左侧又个测试按钮,点击后能够单独测试某个函数。

Kiwi 是一个强大的 BDD 框架,适合稍微复杂写的项目,写法舒服、功能强大,模拟对象、存根语法、异步测试等知足几乎全部的测试场景。不能和 XCTest 继承。

Specta 也是一个 BDD 框架,基于 XCTest 开发,能够和 XCTest 模版集合使用。相比 Kiwi,Specta 轻量一些。开发中通常搭配 Excepta 使用。若是须要使用 Mock 和 Stud 能够搭配 OCMock。

Excepta 是一个匹配框架,比 XCTest 的断言则更加全面一些。

没办法说哪一个最好、最合理,根据项目需求选择合适的组合。

5、网络测试

咱们在测试某个方法的时候可能会遇到方法内部调用了网络通讯能力,网络请求成功,可能刷新 UI 或者给出一些成功的提示;网络失败或者网络不可用则给出一些失败的提示。因此须要对网络通讯去看进行模拟。

iOS 中不少网络都是基于 NSURL 系统下的类实现的。因此咱们能够利用 NSURLProtocol 的能力来监控网络并 mock 网络数据。若是感兴趣能够查看这篇文章

开源项目 OHHTTPStubs 就是一个对网络模拟的库。它能够拦截 HTTP 请求,返回 json 数据,定制各类头信息。

Stub your network requests easily! Test your apps with fake network data and custom response time, response code and headers!

几个主要类及其功能:HTTPStubsProtocol 拦截网络请求;HTTPStubs 单例管理 HTTPStubsDescriptor 实例对象;HTTPStubsResponse 伪造 HTTP 请求。

HTTPStubsProtocol 继承自 NSURLProtocol,能够在 HTTP 请求发送以前对 request 进行过滤处理

+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
   BOOL found = ([HTTPStubs.sharedInstance firstStubPassingTestForRequest:request] != nil);
   if (!found && HTTPStubs.sharedInstance.onStubMissingBlock) {
      HTTPStubs.sharedInstance.onStubMissingBlock(request);
   }
   return found;
}

firstStubPassingTestForRequest 方法内部会判断请求是否须要被当前对象处理

紧接着开始发送网络请求。实际上在 - (void)startLoading 方法中能够用任何网络能力去完成请求,好比 NSURLSession、NSURLConnection、AFNetworking 或其余网络框架。OHHTTPStubs 的作法是获取 request、client 对象。若是 HTTPStubs 单例中包含 onStubActivationBlock 对象,则执行该 block,而后利用 responseBlock 对象返回一个 HTTPStubsResponse 响应对象。

OHHTTPStubs 的具体 API 能够查看文档

举个例子,利用 Kiwi、OHHTTPStubs 测试离线包功能。代码以下

@interface HORouterManager (Unittest)

- (void)fetchOfflineInfoIfNeeded;

@end

SPEC_BEGIN(HORouterTests)

describe(@"routerTests", ^{
    context(@"criticalPath", ^{
        __block HORouterManager *routerManager = nil;
        beforeAll(^{
            routerManager = [[HORouterManager alloc] init];
        });
        it(@"getLocalPath", ^{
            __block NSString *pagePath = nil;
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                pagePath = [routerManager filePathOfUrl:@"http://***/resource1"];
            });
            [[expectFutureValue(pagePath) shouldEventuallyBeforeTimingOutAfter(5)] beNonNil];
            
            __block NSString *rescPath = nil;
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                rescPath = [routerManager filePathOfUrl:@"http://***/resource1"];
            });
            [[expectFutureValue(rescPath) shouldEventuallyBeforeTimingOutAfter(5)] beNonNil];
        });
        it(@"fetchOffline", ^{
            [HOOfflineManager sharedInstance].offlineInfoInterval = 0;
            [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) {
                return [request.URL.absoluteString containsString:@"h5-offline-pkg"];
            } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) {
                NSMutableDictionary *dict = [NSMutableDictionary dictionary];
                dict[@"code"] = @(0);
                dict[@"data"] = @"f722fc3efce547897819e9449d2ac562cee9075adda79ed74829f9d948e2f6d542a92e969e39dfbbd70aa2a7240d6fa3e51156c067e8685402727b6c13328092ecc0cbc773d95f9e0603b551e9447211b0e3e72648603e3d18e529b128470fa86aeb45d16af967d1a21b3e04361cfc767b7811aec6f19c274d388ddae4c8c68e857c14122a44c92a455051ae001fa7f2b177704bdebf8a2e3277faf0053460e0ecf178549e034a086470fa3bf287abbdd0f79867741293860b8a29590d2c2bb72b749402fb53dfcac95a7744ad21fe7b9e188881d1c24047d58c9fa46b3ebf4bc42a1defc50748758b5624c6c439c182fe21d4190920197628210160cf279187444bd1cb8707362cc4c3ab7486051af088d7851846bea21b64d4a5c73bd69aafc4bb34eb0862d1525c4f9a62ce64308289e2ecbc19ea105aa2bf99af6dd5a3ff653bbe7893adbec37b44a088b0b74b80532c720c79b7bb59fda3daf85b34ef35";
                NSData *data = [NSJSONSerialization dataWithJSONObject:dict options:0 error:nil];
                return [OHHTTPStubsResponse responseWithData:data
                                                  statusCode:200
                                                     headers:@{@"Content-Type":@"application/json"}];
            }];
            [routerManager fetchOfflineInfoIfNeeded];
            [[HOOfflineInfo shouldEventually] receive:@selector(saveToLocal:)];
        });
    });
});

SPEC_END

😂 插一嘴,我贴的代码已经好几回能够看到不一样的测试框架组合了,因此不是说选了框架 A 就完事,根据场景选择最优解。

6、UI 测试

上面文章大篇幅的讲了单元测试相关的话题,单元测试十分适合代码质量、逻辑、网络等内容的测试,可是针对最终产物 App 来讲单元测试就不太适合了,若是测试 UI 界面的正确性、功能是否正确显然就不太适合了。Apple 在 Xcode 7 开始推出的 UI Testing 就是苹果本身的 UI 测试框架。

不少 UI 自动化测试框架的底层实现都依赖于 Accessibility,也就是 App 可用性。UI Accessibility 是 iOS 3.0 引入的一我的性化功能,帮助身体不便的人士方便使用 App。

Accessibility 经过对 UI 元素进行分类和标记。分类成相似按钮、文本框、文本等类型,使用 identifier 来区分不一样 UI 元素。无痕埋点的设计与实现里面也使用 accessibilityIdentifier 来绑定业务数据。

  1. 使用 Xcode 自带的 UI测试则在建立工程的时候须要勾选 “Include UI Tests”。
  2. 像单元测试意义,UI 测试方法命名以 test 开头。将鼠标光标移到方法内,点击 Xcode 左下方的红色按钮,开始录制 UI 脚本。

UI 脚本录制

解释说明:

/*! Proxy for an application that may or may not be running. */
@interface XCUIApplication : XCUIElement
// ...
@end
  • XCUIApplication launch 来启动测试。XCUIApplication 是 UIApplication 在测试进程中的代理,用来和 App 进行一些交互。
  • 使用 staticTexts来获取当前屏幕上的静态文本(UILabel)元素的代理。等价于 [app descendantsMatchingType:XCUIElementTypeStaticText]。XCUIElementTypeStaticText 参数是枚举类型。

    typedef NS_ENUM(NSUInteger, XCUIElementType) {
        XCUIElementTypeAny = 0,
        XCUIElementTypeOther = 1,
        XCUIElementTypeApplication = 2,
        XCUIElementTypeGroup = 3,
        XCUIElementTypeWindow = 4,
        XCUIElementTypeSheet = 5,
        XCUIElementTypeDrawer = 6,
        XCUIElementTypeAlert = 7,
        XCUIElementTypeDialog = 8,
        XCUIElementTypeButton = 9,
        XCUIElementTypeRadioButton = 10,
        XCUIElementTypeRadioGroup = 11,
        XCUIElementTypeCheckBox = 12,
        XCUIElementTypeDisclosureTriangle = 13,
        XCUIElementTypePopUpButton = 14,
        XCUIElementTypeComboBox = 15,
        XCUIElementTypeMenuButton = 16,
        XCUIElementTypeToolbarButton = 17,
        XCUIElementTypePopover = 18,
        XCUIElementTypeKeyboard = 19,
        XCUIElementTypeKey = 20,
        XCUIElementTypeNavigationBar = 21,
        XCUIElementTypeTabBar = 22,
        XCUIElementTypeTabGroup = 23,
        XCUIElementTypeToolbar = 24,
        XCUIElementTypeStatusBar = 25,
        XCUIElementTypeTable = 26,
        XCUIElementTypeTableRow = 27,
        XCUIElementTypeTableColumn = 28,
        XCUIElementTypeOutline = 29,
        XCUIElementTypeOutlineRow = 30,
        XCUIElementTypeBrowser = 31,
        XCUIElementTypeCollectionView = 32,
        XCUIElementTypeSlider = 33,
        XCUIElementTypePageIndicator = 34,
        XCUIElementTypeProgressIndicator = 35,
        XCUIElementTypeActivityIndicator = 36,
        XCUIElementTypeSegmentedControl = 37,
        XCUIElementTypePicker = 38,
        XCUIElementTypePickerWheel = 39,
        XCUIElementTypeSwitch = 40,
        XCUIElementTypeToggle = 41,
        XCUIElementTypeLink = 42,
        XCUIElementTypeImage = 43,
        XCUIElementTypeIcon = 44,
        XCUIElementTypeSearchField = 45,
        XCUIElementTypeScrollView = 46,
        XCUIElementTypeScrollBar = 47,
        XCUIElementTypeStaticText = 48,
        XCUIElementTypeTextField = 49,
        XCUIElementTypeSecureTextField = 50,
        XCUIElementTypeDatePicker = 51,
        XCUIElementTypeTextView = 52,
        XCUIElementTypeMenu = 53,
        XCUIElementTypeMenuItem = 54,
        XCUIElementTypeMenuBar = 55,
        XCUIElementTypeMenuBarItem = 56,
        XCUIElementTypeMap = 57,
        XCUIElementTypeWebView = 58,
        XCUIElementTypeIncrementArrow = 59,
        XCUIElementTypeDecrementArrow = 60,
        XCUIElementTypeTimeline = 61,
        XCUIElementTypeRatingIndicator = 62,
        XCUIElementTypeValueIndicator = 63,
        XCUIElementTypeSplitGroup = 64,
        XCUIElementTypeSplitter = 65,
        XCUIElementTypeRelevanceIndicator = 66,
        XCUIElementTypeColorWell = 67,
        XCUIElementTypeHelpTag = 68,
        XCUIElementTypeMatte = 69,
        XCUIElementTypeDockItem = 70,
        XCUIElementTypeRuler = 71,
        XCUIElementTypeRulerMarker = 72,
        XCUIElementTypeGrid = 73,
        XCUIElementTypeLevelIndicator = 74,
        XCUIElementTypeCell = 75,
        XCUIElementTypeLayoutArea = 76,
        XCUIElementTypeLayoutItem = 77,
        XCUIElementTypeHandle = 78,
        XCUIElementTypeStepper = 79,
        XCUIElementTypeTab = 80,
        XCUIElementTypeTouchBar = 81,
        XCUIElementTypeStatusItem = 82,
    };
  • 经过 XCUIApplication 实例化对象调用 descendantsMatchingType: 方法获得的是 XCUIElementQuery 类型。好比 @property (readonly, copy*) XCUIElementQuery *staticTexts;

    /*! Returns a query for all descendants of the element matching the specified type. */
    - (XCUIElementQuery *)descendantsMatchingType:(XCUIElementType)type;
  • descendantsMatchingType 返回全部后代的类型匹配对象。childrenMatchingType 返回当前层级子元素的类型匹配对象

    /*! Returns a query for direct children of the element matching the specified type. */
    - (XCUIElementQuery *)childrenMatchingType:(XCUIElementType)type;
  • 拿到 XCUIElementQuery 后不能直接拿到 XCUIElement。和 XCUIApplication 相似,XCUIElement 不能直接访问 UI 元素,它是 UI 元素在测试框架中的代理。能够经过 Accessibility 中的 frameidentifier 来获取。

对比不少自动化测试框架都须要找出 UI 元素,也就是借助于 Accessibilityidentifier。这里的惟一标识生成对比为 UIAutomation 添加自动化测试标签的探索]

第三方 UI 自动化测试框架挺多的,能够查看下典型的 appiummacaca

7、 测试经验总结

TDD 写好测试再写业务代码,BDD 先写实现代码,再写基于行为的测试代码。另外一种思路是不必针对每一个类的私有方法或者每一个方法进行测试,由于等所有功能作完后针对每一个类的接口测试,通常会覆盖据大多数的方法。等测试完看若是方法未被覆盖,则针对性的补充 Unit Test

目前,UI 测试(appium) 仍是建议在核心逻辑且长时间没有改动的状况下去作,这样子每次发版本的时候能够看成核心逻辑回归了,目前来看价值是方便后续的迭代和维护上有一些便利性。其余的功能性测试仍是走 BDD。

对于类、函数、方法的走 TDD,老老实实写 UT、走 UT 覆盖率的把控。

UITesting 仍是建议在核心逻辑且长时间没有改动的状况下去作,这样子每次发版本的时候能够看成核心逻辑回归,目前来看价值是方便后续的迭代和维护上有一些便利性。例如用户中心 SDK 升级后,当时有了UITesing,基本上免去了测试人员介入。

若是是一些活动页和逻辑常常变更的,老老实实走测试黑盒...

我以为一直有个误区,就是以为自动测试是为了质量,其实质量都是附送的,测试先行是让开发更快更爽的

测试占比

WWDC 这张图也很清楚,UI 其实须要的占比较小,仍是要靠单测驱动。

参考资料