iOS 单元测试(Unit Test 和 UI Test)

一 :前言

很多的开发者 都听说过单元测试,但是不可否认 很多开发者 在实际开发中很少使用这个 单元测试。 大部分人想我自己把工程跑起来 一步步 按照 流程来测试 就行了 。在新建任何一个 工程时 很多小伙伴 对如下图 所示的 的 Include Unit Tests 和 Include UI Tests  感到疑惑。


why

其实这两个 就是 Xcode 自带的 UnitTest 。

百度百科 对于单元测试 的定义 如下

单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。对于单元测试中单元的含义,一般来说,要根据实际情况去判定其具体含义,如C语言中单元指一个函数,Java里单元指一个类,图形化的软件中可以指一个窗口或一个菜单等。总的来说,单元就是人为规定的最小的被测功能模块。单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。

二: 为什么需要使用 单元测试

1.0  设想你在在一个庞大的工程里面加个了 小功能 A ,按照我们前面的说法,是不是每次都要把工程跑起来,然后 去到 A处 然后进行测试。甚至如果我们仅仅是想 测试一个 接口 返回的参数 而这个接口又需要使用到现有工程中的很多参数 等 如果使用 单元测试的话 就不用每次去 把整个工程跑起来,只需要跑你测试的部分。

2.0 我们可以使用单元测试测 某个方法的耗时和性能,单次 和 多次运行的整体对比的。当然你可以在 方法执行前 获取时间 ,方法结束 后获取时间等方式 获取时间消耗 姑且这样写 麻烦不说 但是如何 计算 CPU占用这些消耗呢?当然我们可以使用instrument 来做更专业的测试。 相对而言 单元测试更加便捷 和 方便使用给我们省不少事。

三 :如何做单元测试

本文只介绍Xcode 自带的 Unit Tests  和 UI Tests

代码下载地址: https://gitee.com/DeLongYang/Performace_Test  下的 UnitTestDemoTests 文件夹就是了。

水平有限 ,如有 不对 希望大家 不吝指正。工程需要安装 Pods 。 

第一 如何添加单元测试

1.0  新建工程时 也就是  前言中的那两个选项 勾选就可以了。

2.0 在现有的 工程中 添加单元测试 File ->New ->Target 在选项卡中 选择 iOS UI Testing Bundle 和 iOS Unit Testing Bundle 这两个 任选一个添加给 指定的工程就可以了。

下面我们分   UI Testing Bundle  测试  和  UITest  和 性能测试来 分别阐述。 


第二: iOS Unit Tests Bundle  

1.0  测试方法

打开 UnitTestDemoTestsTests.m  文件 ,如下图所示 鼠标光标选中菱形区域 就会 显示这个播放的状态,然后

点击 这个播放的状态 就会测试这个方法。 或者  选择左边 testIsChinese 右边也会出现 黑色的小播放按钮 点击也可以测试。

b

2.0  添加测试方法

下面这些是 系统自带的方法 苹果都给了我们注释。 

- (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.

}

这是 我们做性能测试的方法   把要测试的方法 放进Block 就可以了 。后面会重点介绍 

- (void)testPerformanceExample {

    // This is an example of a performance test case.

    [self measureBlock:^{

        // Put the code you want to measure the time of here.

        for (int i =0; i<1000; i++) {

            NSLog(@"this is a example");

        }

    }];

}

注意!! 所有需要测试的方法名以 test 开头 。 刚添加进去的方法可能左上角的 菱形按钮 不会立即显示 ,你可以 Comand + U  Build 这个测试文件一下 就可以了 或者  选中 - (void)testPerformanceExample  测下这个方法就可以了 或者 从 图b 中 3  处 测试都可以。

整体的测试方法 和 Java 中的Junit 有点类似 。 测试没通过 左边就会有一个 大红叉  ,能通过的方法 就会有一个 绿色的钩。 

- (void)testExample {

    // This is an example of a functional test case.

    // Use XCTAssert and related functions to verify your tests produce the correct results.


    NSLog(@"自定义测试testExample");

    int a = 3;

    XCTAssertTrue(a == 0,"a 不能 等于 0");


}

下面是一些  常用的   测试语法。 注意  空 和 nil  还是有区别的 。 读者自己可以测试一下。 

- (void)testAssertSyntax

{

//    XCTFail(@"this is a fail test");  // 生成一个失败的测试

//    XCTAssertNil(@"not a nil string",@"string must be nil"); // XCTAssertNil(a1, format...) 为空判断, a1 为空时通过,反之不通过;

//    XCTAssertNil(@"",@"string must be nil");  // 注意@"" 一样无法通过

    XCTAssertNil(nil,@"object must be nil");

    // XCTAssertNotNil(a1, format…) 不为nil 判断,a1不为  nil 时通过,反之不通过;

    // 注意空  和 nil 还是有区别的

    XCTAssertNotNil(@"not nil string", @"string can not be nil");

//    XCTAssert(expression, format...) 当expression求值为TRUE时通过; expression 为一个表达式

//    XCTAssert((2 > 2), @"expression must be true");

    XCTAssert((3>2),@"expression is true");

    // XCTAssertTrue(expression, format...) 当expression求值为TRUE时通过;>0 的都视为 true

    XCTAssertTrue(1, @"Can not be zero");

    // XCTAssertFalse(expression, format...) 当expression求值为False时通过;

    XCTAssertFalse((2 < 2), @"expression must be false");

    // XCTAssertEqualObjects(a1, a2, format...) 判断相等, [a1 isEqual:a2] 值为TRUE时通过,其中一个不为空时,不通过;

    XCTAssertEqualObjects(@"1", @"1", @"[a1 isEqual:a2] should return YES");

    //    XCTAssertEqualObjects(@"1", @"2", @"[a1 isEqual:a2] should return YES");

    //    XCTAssertNotEqualObjects(a1, a2, format...) 判断不等, [a1 isEqual:a2] 值为False时通过,

//        XCTAssertNotEqualObjects(@"1", @"1", @"[a1 isEqual:a2] should return NO");

    XCTAssertNotEqualObjects(@"1", @"2", @"[a1 isEqual:a2] should return NO");

    // XCTAssertEqual(a1, a2, format...) 判断相等(当a1和a2是 C语言标量、结构体或联合体时使用,实际测试发现NSString也可以);

    // 1.比较基本数据类型变量

    //    XCTAssertEqual(1, 2, @"a1 = a2 shoud be true"); // 无法通过测试

    XCTAssertEqual(1, 1, @"a1 = a2 shoud be true"); // 通过测试

    // 2.比较NSString对象

    NSString *str1 = @"1";

    NSString *str2 = @"1";

//    NSString *str3 = str1;

    XCTAssertEqual(str1, str2, @"a1 and a2 should point to the same object"); // 通过测试

    //    XCTAssertEqual(str1, str3, @"a1 and a2 should point to the same object"); // 通过测试

    // 3.比较NSArray对象

        NSArray *array1 = @[@1];

        NSArray *array2 = @[@1];

        NSArray *array3 = array1;

//    XCTAssertEqual(array1, array2, @"a1 and a2 should point to the same object"); // 无法通过测试

    XCTAssertEqual(array1, array3, @"a1 and a2 should point to the same object"); // 通过测试

    // XCTAssertNotEqual(a1, a2, format...) 判断不等(当a1和a2是 C语言标量、结构体或联合体时使用);

    // XCTAssertEqualWithAccuracy(a1, a2, accuracy, format...) 判断相等,(double或float类型)提供一个误差范围,当在误差范围(+/- accuracy )以内相等时通过测试;

//    XCTAssertEqualWithAccuracy(1.0f, 1.5f, 0.25f, @"a1 = a2 in accuracy should return NO");  // 测试没法通过

    // XCTAssertNotEqualWithAccuracy(a1, a2, accuracy, format...) 判断不等,(double或float类型)提供一个误差范围,当在误差范围以内不等时通过测试;

    XCTAssertNotEqualWithAccuracy(1.0f, 1.5f, 0.25f, @"a1 = a2 in accuracy should return NO");  // 测试通过 

    // XCTAssertThrows(expression, format...) 异常测试,当expression发生异常时通过;反之不通过;(很变态)

    // XCTAssertThrowsSpecific(expression, specificException, format...) 异常测试,当expression发生 specificException 异常时通过;反之发生其他异常或不发生异常均不通过;

    // XCTAssertThrowsSpecificNamed(expression, specificException, exception_name, format...) 异常测试,当expression发生具体异常、具体异常名称的异常时通过测试,反之不通过;

    // XCTAssertNoThrow(expression, format…) 异常测试,当expression没有发生异常时通过测试;

    // XCTAssertNoThrowSpecific(expression, specificException, format...)异常测试,当expression没有发生具体异常、具体异常名称的异常时通过测试,反之不通过;

    // XCTAssertNoThrowSpecificNamed(expression, specificException, exception_name, format...) 异常测试,当expression没有发生具体异常、具体异常名称的异常时通过测试,反之不通过

}

像 我们前文 中说到的  测试某个接口,源码中的  testRequest 方法。 这里就不贴了 。 

/**

测试 网络请求的方法

*/

- (void)testRequest

{

       。。。。。。

}

这里 如果是新建的工程的话 运行报错 。提示如下:

App Transport Security has blocked a cleartext HTTP (http://) resource load since it is insecure. Temporary exceptions can be configured via your app’s Info.plist file.

 请参考http://blog.csdn.net/maxdong24/article/details/53610127中的方案解决。

这是 iOS9 新增加的 传输安全性的规定。

3.0  新建 自定义的 测试文件

Command + N   选中Unit Test Case Class  不是 Cocoa  Touch Class  !!!!  笔者习惯性思维 好几次选错。新建

UserTest.m 类, 同时在工程中 新建 User 类 ,具体的看源代码中的。因为没有做UI测试 所以  setUp

中的一些方法 我们注释掉不然会 报错。 

第二: iOS UI Testing 

打开 UnitTestDemoTestsUITests.m 发现 setUp 方法 和 UnitTest 中有些不同。多了一个 

self.continueAfterFailure = NO; 和   [[[XCUIApplication alloc] init] launch];  苹果都有解释 。我们照做就可以了。

- (void)setUp {

    [super setUp];

    // Put setup code here. This method is called before the invocation of each test method in the class.

// 如果发生 测试不通过的情况 ,最好 停止程序的运行 

// In UI tests it is usually best to stop immediately when a failure occurs.

    self.continueAfterFailure = NO;

//  UI 测试必须 等应用 先开启 ,这个方法 可以确保应用的开启 在每个测试方法 测试的时候 。

    // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method.

    [[[XCUIApplication alloc] init] launch];


    // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.

}

先 添加 测试方法 - (void)testLogin  大扩号中是 空白的

- (void)testLogin

{

}

 把 光标 放进到这个 大括号里面,看到 如下图 c  的小红点。点击这个录制屏幕的按钮。应用启动。 我们点击

首页的 ViewController 中的两个  userName 和  passWord UITextField 输入相关的内容。 再点登陆 发现 - (void)testLogin的括号中自动生成了代码 。 

c

接着  直接测试 - (void)testLogin  会发现重复 了我们录屏时候的 输入和 输出 包括跳转等等。

注意 在 OC 写的工程中 如果 输入中文 自动生成的测试代码有问题如图 d 所示

- (void)testLoginTwo 就是这种现象 的代码

d

但是在 swift 工程 中就没有这种问题。 如何解决?

其实我们可以 自己 写 测试 代码  如  - (void)testLoginThree  我们可以打断点 看 打印 app 的结构 。 

- (void)testLoginThree

{

    XCUIApplication *app = [[XCUIApplication alloc] init];

    // 下面这是一种通过 遍历 获取的形式

    for (NSInteger i = 0;i < app.textFields.count; i++) {

        if ([[app.textFields elementBoundByIndex:i] exists]) {//判断是否存在

            [[app.textFields elementBoundByIndex:i] tap];//输入框要获取焦点后才能给输入框自动赋值

            if (i == 0 ) {

                // 给 第一个userName 自动赋值 你好

                [[app.textFields elementBoundByIndex:i] typeText:@"你好"];

            }

            // 给 第二个userPass 自动赋值 德龙

            if (i == 1) {

                  [[app.textFields elementBoundByIndex:i] typeText:@"德龙"];

            }

        }

    }

}

下面看另外一种写法  这种发放比较 特别 

我们之所以 能使用 textFields[@"username:"];  和  app.buttons[@"login"]  这种语法 来找到对应的控件 是因为

usernameTextField 的Placeholder 是 username:  而  登录按钮的title 是 login  !!!storyboard中都有设置 。 不然会报错。

- (void)testLoginFour

{

    XCUIApplication *app = [[XCUIApplication alloc] init];

//    //XCUIElement 这是 UI 元素的代理。元素都有类型和唯一标识。可以结合使用来找到元素在哪里,如当前界面上的一个输入框

    XCUIElementQuery *textFields = app.textFields;

//    XCUIElement *usernameTextField = [textFields objectForKeyedSubscript:@"username:"];

    XCUIElement *usernameTextField = textFields[@"username:"];

    [usernameTextField tap];

    [usernameTextField typeText:@"德龙"];

    XCUIElement *passwordTextField = app.textFields[@"password:"];

    [passwordTextField tap];

    [passwordTextField tap];

    [passwordTextField typeText:@"杨"];


    //

    [app.buttons[@"login"] tap];


    //登录成功后的控制器的title为loginSuccess,只需判断控制器的title时候一样便可判断登录是否成功

    NSLog(@"title is %@",app.navigationBars.element.identifier);

  XCTAssertEqualObjects(app.navigationBars.element.identifier, @"loginSuccess");


    // 延时 3s 再消失

    XCUIElement *window = [app.windows elementBoundByIndex:0];

    [window pressForDuration:3];

}

核心类是 XCUIElement  和 XCUIElementQuery  这两个类 , 可以看下这两个类 看下是如何设计的。 这种设计思路 笔者认为 和FMDB 中的 FMResult 等的设计思路是差不多的。 另外还有要给  TableViewController 的测试

这里就不再赘述了。

第三: 性能测试

我们回到 UnitTestDemoTestsTests.m 中的 - (void)testPerformanceExample 方法 写入

- (void)testPerformanceExample {

    // This is an example of a performance test case.

    [self measureBlock:^{

        // Put the code you want to measure the time of here.

        for (int i =0; i<1000; i++) {

            NSLog(@"this is a example");

        }

    }];

}

第一次运行 会出现 改方法的耗时 ,然后 提示 noBase ...  点进这个提示 我们可以设置base 设置之后再运行就点击左边的 灰色的 钩 会出现下图e 所示的


e


对比 。 耗时和CPU 消耗等,以及多次的 对比都有 ,比较简单 。 

到这就结束了 , 满地卖萌打滚求喜欢 ,各位看官打赏打赏?

二: 参考文档

https://www.jianshu.com/p/8bbec078cabe

https://www.jianshu.com/p/07cfc17916e8

http://www.jianshu.com/p/560e397efdc7

推荐阅读更多精彩内容

  • 单元测试不是一个小工程,需要多用些时间才能做好,不要希望通过这个文章就能掌握单元测试,这只是一个入门,需要自己动手...
    勇不言弃92阅读 4,945评论 10 52
  • 前言 单元测试和UI测试大致步骤网上很多文章都有,如果会的可以忽略,关键是错误总结,网上很少有文章提及到,感兴趣的...
    _YGL_阅读 4,269评论 22 23
  • 转自:http://www.jianshu.com/p/009844a0b9ed 什么是单元测试?一听到单元测试这...
    YYT1992阅读 401评论 0 2
  • /*------------使用Xcode自带单元测试UnitTest-----*/ /* 单元测试:就是为...
    彬至睢阳阅读 64评论 0 0
  • 领导给的需求是: 关于单元测试的任务反馈: http://www.jianshu.com/p/d15a7dea0c...
    SOI阅读 3,680评论 1 34