iOS 单元测试

2023-02-22  本文已影响0人  搬砖的crystal

一、简介

1.简介

单元测试(Unit Testing),又称为模块测试,是指对软件中的最小可测试单元进行检查和验证,通过开发者编写代码去验证被测代码是否正确的一种手段,例如编写一个测试函数去测试某一功能函数是否能正确执行达到预期效果。在实际项目开发中使用单元测试可以提高软件的质量,也可以尽量早的发现代码中存在的问题加以修正。

执行单元测试,是为了证明某段代码的行为确实和开发者所期望的一致。因此,我们所要测试的是规模很小的、非常独立的功能片段。通过对所有单独部分的行为建立起信心。然后,才能开始测试整个系统。

2.应用

持续集成(Continuous Integration),简称 CI,是软件开发周期的一种实践,把代码仓库(Gitlab 或者 Github)、构建工具(如 Jenkins)和测试工具(SonarQube)集成在一起,频繁的将代码合并到主干然后自动进行构建和测试。简单来说持续集成就是一个监控版本控制系统中代码变化的工具,当发生变化是可以自动编译和测试以及执行后续自定义动作。

二、使用

1.创建测试项目

在创建项目时勾选 Include Tests 选项,如下图所示:


创建项目成功后,项目目录下即可看到对应的单元测试文件夹下的 ZJHUnitTestDemoTests 文件

如果之前的项目还没有添加单元测试 target,也可以按照下图方式进行新建:

2.单元测试类介绍

在新建的测试文件代码如下所示,系统自动生成了几个方法:

#import <XCTest/XCTest.h>

// 所有的测试类需要继承 XCTestCase
@interface ZJHUnitTestDemoTests : XCTestCase

@end

@implementation ZJHUnitTestDemoTests

/// 在每一个测试方法调用前,都会被调用;用来初始化 test 用例的一些初始值
- (void)setUp {
    // Put setup code here. This method is called before the invocation of each test method in the class.
    // 在这里设置代码。在调用类中的每个测试方法之前调用此方法。
}

/// 在每一个测试方法调用后,都会被调用;用来重置 test 方法的数值
- (void)tearDown {
    // Put teardown code here. This method is called after the invocation of each test method in the class.
    // 在这里输入删除代码。在调用类中的每个测试方法之后调用此方法。
}

/// 测试方法命名以 test 开始
- (void)testExample {
    // This is an example of a functional test case.
    // Use XCTAssert and related functions to verify your tests produce the correct results.
    // 这是一个功能测试用例。
    // 使用XCTAssert和相关函数来验证您的测试产生正确的结果。
}

/// 性能测试
- (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
3.新建示例

我们在项目里面创建一个 ZJHMathTool 类:

@interface ZJHMathTool : NSObject
- (int)sumA:(int)a andB:(int)b;
- (int)subA:(int)a andB:(int)b;
- (int)multiplyA:(int)a andB:(int)b;
- (int)divideA:(int)a andB:(int)b;
@end

@implementation ZJHMathTool
- (int)sumA:(int)a andB:(int)b {
    return a + b;
}

- (int)subA:(int)a andB:(int)b {
    return a - b;
}

- (int)multiplyA:(int)a andB:(int)b {
    return a * b;
}

- (int)divideA:(int)a andB:(int)b {
    return a / b;
}
@end

同时在新建一个对应的 ZJHMathToolTests 测试类:

#import <XCTest/XCTest.h>

@interface ZJHMathToolTests : XCTestCase
@end

@implementation ZJHMathToolTests

- (void)setUp {
}

- (void)tearDown {
}

- (void)testExample {
}

- (void)testPerformanceExample {
    [self measureBlock:^{
    }];
}
@end
4.逻辑测试

接下来我们开始编写用例,来测试 ZJHMathTool 中的方法,如下所示

@interface ZJHMathToolTests : XCTestCase
@property (nonatomic, strong) ZJHMathTool *mathTool;
@end

@implementation ZJHMathToolTests

// 新建ZJHMathTool对象
- (void)setUp {
    self.mathTool = [ZJHMathTool new];
}

// 销毁ZJHMathTool对象
- (void)tearDown {
    self.mathTool = nil;
}

// 测试加法
- (void)testMathAdd {
    int result = [self.mathTool sumA:2 andB:3];
    XCTAssert(result == 5, @"加法计算出错");
}

// 测试减法
- (void)testMathSub {
    int result = [self.mathTool subA:5 andB:2];
    XCTAssert(result == 3, @"减法计算出错");
}

@end

运行测试用例 :
代码编辑器边栏菱形按钮,测试单个用例
Test 导航栏,测试单个用例
快捷键 command + U 测试全部用例
使用命令行工具 xcodebuild 可以测试单个用例,也可以测试全部用例

5.性能测试

性能测试通过度量代码块执行所消耗的时间长短,来衡量是否通过测试。

(1)测试方法准备

新建 ZJHPerson 类,然后添加一个循环打印方法。

@interface ZJHPerson : NSObject
- (void)sayHello;
@end

@implementation ZJHPerson
- (void)sayHello {
    for (int i = 0; i < 1000; i++) {
        NSLog(@"hello");
    }
}
@end

然后再新建 ZJHPerson 对象测试类 ZJHPersonTests

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

@interface ZJHPersonTests : XCTestCase
@property (nonatomic, strong) ZJHPerson *person;
@end

@implementation ZJHPersonTests
- (void)setUp {
    self.person = [ZJHPerson new];
}
- (void)tearDown {
    self.person = nil;
}
- (void)testPerformanceExample {
    [self measureBlock:^{
        [self.person sayHello];
    }];
}
@end
(2)性能测试 API

有两个 API 可以使用

- (void)testPerformanceOfMyFunction {
        [self measureBlock:^{
            //做你想测量的东西。
            MyFunction();
        }];
  }
- (void)testMyFunction2_WallClockTime {
        [self measureMetrics:[self class].defaultPerformanceMetrics automaticallyStartMeasuring:NO forBlock:^{
            // 做设置工作,需要为每个迭代,但你不希望在调用- startmeasurement之前进行测量
            SetupSomething();
            [self startMeasuring];

            // 做你想测量的东西。
            MyFunction();
            [self stopMeasuring];

            //执行每次迭代都需要执行的分解工作,但你不想在调用- stopmeasurement后进行度量
            TeardownSomething();
        }];
    }
(3)设置基准线

所有的性能测试需要设置一个 Baseline 来验证是否通过测试,没有设置的会提示 No baseline average for Time。点击左边灰色菱形图标可查看性能测试结果。



在性能测试结果图里可以看到平均时间(总时长/10),还有 10 个柱状图,这个意思是在这个测试方法运行总时长被分为 10 份,蓝色柱子表示每份的耗时,中间的横线表示平均时间,点击数字可查看每份中的平均时长。

6.异步测试

什么时候需要使用异步测试:
打开文档、在后台线程中执行的服务和网络活动、执行动画、UI 测试时

(1)异步测试 XCTestExpectation

异步测试分为3个部分: 新建期望 、等待期望被履行和履行期望 。

/// 异步测试XCTestExpectation:测试类持有期望
- (void)testAsyncMethod1 {
    // 新建期望:测试类持有的初始化方法
    XCTestExpectation *expect1 = [self expectationWithDescription:@"asyncTest1"];

    // 履行期望:执行异步操作
    [ZJHNetworkTool requestUrl:@"getTestData" param:@{} completion:^(NSDictionary * _Nonnull respondDic) {
        // 异步结束,标注期望达成
        [expect1 fulfill];
    }];
    
    // 等待期望被履行:测试类持有时的等待方法
    [self waitForExpectationsWithTimeout:3.0 handler:^(NSError * _Nullable error) {
        NSLog(@"***ZJH error : %@", error);
    }];
}

/// 异步测试XCTestExpectation:自己类持有期望
- (void)testAsyncMethod2 {
    // 新建期望:自己持有的初始化方法
    XCTestExpectation *expect2 = [[XCTestExpectation alloc] initWithDescription:@"asyncTest2"];
    
    
    // 履行期望:执行异步操作
    [ZJHNetworkTool requestUrl:@"getTestData" param:@{} completion:^(NSDictionary * _Nonnull respondDic) {
        XCTAssertTrue([respondDic[@"code"] isEqualToString:@"200"]);
        // 异步结束,标注期望达成
        [expect2 fulfill];
    }];

    // 等待期望被履行:自己持有时的等待方法
    [self waitForExpectations:@[expect2] timeout:3];
}
(2)异步测试 XCTWaiter

XCTWaiter 是 2017 年新增的异步测试方案,可以通过代理方式来处理异常情况。
XCTWaiterDelegate :如果委托是 XCTestCase 实例,下方代理被调用时会报告为测试失败。

/// 异步测试XCTWaiter
- (void)testAsyncMethod3 {
    // 新建期望
    XCTWaiter *waiter = [[XCTWaiter alloc] initWithDelegate:self];
    XCTestExpectation *expect3 = [[XCTestExpectation alloc] initWithDescription:@"asyncTest3"];
        
    // 履行期望:执行异步操作
    [ZJHNetworkTool requestUrl:@"getTestData" param:@{} completion:^(NSDictionary * respondDic) {
        XCTAssertTrue([respondDic[@"code"] isEqualToString:@"200"]);
        // 异步结束,标注期望达成
        [expect3 fulfill];
    }];

    // 等待期望被履行
    XCTWaiterResult result = [waiter waitForExpectations:@[expect3]
                                                 timeout:3
                                            enforceOrder:NO];

    XCTAssert(result == XCTWaiterResultCompleted, @"failure: %ld", result);
}

// 如果有期望超时,则调用。
- (void)waiter:(XCTWaiter *)waiter didTimeoutWithUnfulfilledExpectations:(NSArray<XCTestExpectation *> *)unfulfilledExpectations {
    NSLog(@"***ZJH 如果有期望超时,则调用。");
}

// 当履行的期望被强制要求按顺序履行,但期望以错误的顺序被履行,则调用。
- (void)waiter:(XCTWaiter *)waiter fulfillmentDidViolateOrderingConstraintsForExpectation:(XCTestExpectation *)expectation requiredExpectation:(XCTestExpectation *)requiredExpectation {
    NSLog(@"***ZJH 当履行的期望被强制要求按顺序履行,但期望以错误的顺序被履行,则调用。");
}

// 当某个期望被标记为被倒置,则调用。
- (void)waiter:(XCTWaiter *)waiter didFulfillInvertedExpectation:(XCTestExpectation *)expectation {
    NSLog(@"***ZJH 当某个期望被标记为被倒置,则调用。");
}

// 当 waiter 在 fullfill 和超时之前被打断,则调用。
- (void)nestedWaiter:(XCTWaiter *)waiter wasInterruptedByTimedOutWaiter:(XCTWaiter *)outerWaiter {
    NSLog(@"***ZJH 当 waiter 在 fullfill 和超时之前被打断,则调用。");
}

三、其他补充

1.断言记录

在写测试用例的时候,我们可以使用断言,下面是记录一下:

XCTFail(format…) 生成一个失败的测试; 
 
XCTAssertNil(a1, format...)为空判断,a1为空时通过,反之不通过; 
 
XCTAssertNotNil(a1, format…)不为空判断,a1不为空时通过,反之不通过;
 
XCTAssert(expression, format...)当expression求值为TRUE时通过; 
 
XCTAssertTrue(expression, format...)当expression求值为TRUE时通过; 
 
XCTAssertFalse(expression, format...)当expression求值为False时通过; 
 
XCTAssertEqualObjects(a1, a2, format...)判断相等,[a1 isEqual:a2]值为TRUE时通过,其中一个不为空时,不通过;
 
XCTAssertNotEqualObjects(a1, a2, format...)判断不等,[a1 isEqual:a2]值为False时通过;
 
XCTAssertEqual(a1, a2, format...)判断相等(当a1和a2是 C语言标量、结构体或联合体时使用,实际测试发现NSString也可以); 
 
XCTAssertNotEqual(a1, a2, format...)判断不等(当a1和a2是 C语言标量、结构体或联合体时使用);
 
XCTAssertEqualWithAccuracy(a1, a2, accuracy, format...)判断相等,(double或float类型)提供一个误差范围,当在误差范围(+/-accuracy)以内相等时通过测试; 
 
XCTAssertNotEqualWithAccuracy(a1, a2, accuracy, format...) 判断不等,(double或float类型)提供一个误差范围,当在误差范围以内不等时通过测试; 
 
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没有发生具体异常、具体异常名称的异常时通过测试,反之不通过
 
 
特别注意下XCTAssertEqualObjects和XCTAssertEqual。
 
XCTAssertEqualObjects(a1, a2, format...)的判断条件是[a1 isEqual:a2]是否返回一个YES。
 
XCTAssertEqual(a1, a2, format...)的判断条件是a1 == a2是否返回一个YES。
 
对于后者,如果a1和a2都是基本数据类型变量,那么只有a1 == a2才会返回YES。例如
2.查看代码覆盖率
(1)Edit Scheme 勾选配置

调出工程配置 Test->Options->Code Coverage勾选上


(2)结果查看

运行测试后,command+9或者点击工程左上角最后一个图标查看覆盖报告

(3)代码查看

双击方法名或者点击方法名右侧的箭头可以跳转到该方法中。右侧有数字,0 表示没有覆盖掉,1 表示覆盖了一次,调用了几次数字就会变成几。


3.跳过部分测试

在 Xcode 10 中新增功能,在 Edit Scheme -> Test -> Info -> Tests 中可以通过取消勾选,来选择跳过部分测试用例。在 target 的 Options 选项中,Automatically includes new tests,选项是默认勾选的,新建的测试文件会自动添加进去。


4.测试用例的执行顺序

默认情况下,测试用例执行的顺序是按字母顺序来执行的,按固定顺序执行可能会使一些隐式的依赖关系无法被发现。现在有了随机的执行顺序,就可以挖掘出那些隐式的依赖关系。可以在 Edit Scheme -> Test -> Info -> Tests -> Options 中开启该功能。


5.并行测试

并行测试可以同时进行多个测试,从而节省大量时间。在测试时会启动多个模拟器,模拟器之间的数据都是隔离的,可以在 Edit Scheme -> Test -> Info -> Tests -> Options 中开启该功能。如上图
对于并行测试的一些建议:

上一篇 下一篇

猜你喜欢

热点阅读