iOS UnitTest 单元测试(逻辑,同异步,性能,封装)

iOS测试我分三个篇介绍UI 测试后,覆盖率测试,Unit单元测试.
本文介绍下面几个功能逻辑等UnitTest部分:
1.逻辑功能测试
2.同,异步功能方法测试 - [分析AFNetworking解释]
3.单元测试之Mock使用简介
4.性能耗时测试
5.单例测试
6.编写测试用例该注意要点
7.封装测试库
8.自动化测试,Jenkins的安装和使用
9.自动化单元测试,可以看LeanCloud 工程师的李智维的自动化单元测试的直播录影
李智维的演示github李智维的演示github

一 : 逻辑功能测试

(1)直接测试文件简单测试一个字符串是否为nil, 并熟悉XCTAssert

//简单例子
- (void)testExample {
    NSString *name = @"明星";
    XCTAssertNotNil(name, @"btn should not be nil");//报错提示语:@"btn should not be nil"
}

上面简单在testExample中通过XCTAssertNotNil测试一下name是否为nil,点击方法左侧棱形按钮测试.

我们定位XCTAssertNotNil跳转到声明文件XCTestAssertions.h文件,包含很多判断,全部是宏定义方式,下面是网友的中文解释:

 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。

(2) 测试项目中文件中的某个方法 - 没有返回值

<2.1>创建一个LoginViewController文件,并在头文件中加上- (void)loginWithPhone:(NSString *)phone code:(NSString *)code方法:

#import "LoginViewController.h"
@interface LoginViewController ()
@end
@implementation LoginViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
}

//手机验证码登录
- (void)loginWithPhone:(NSString *)phone code:(NSString *)code {
    NSMutableDictionary *dic = @{}.mutableCopy;
    [dic setObject:phone forKey:@"phone"];
    [dic setObject:code forKey:@"code"];
    NSLog(@"%@",dic);
}
@end

<2.2>在Tests文件夹下创建一个测试文件LoginVCtrlTests,创建变量,并调用其loginWithPhone方法

#import <XCTest/XCTest.h>
#import "LoginViewController.h"
@interface LoginVCtrlTests : XCTestCase
@property(nonatomic,strong)LoginViewController *loginVC;
@end

@implementation LoginVCtrlTests

- (void)setUp {
    [super setUp];
    self.loginVC = [[LoginViewController alloc]init];
}

- (void)tearDown {
    [super tearDown];
    self.loginVC = nil;
}

- (void)testExample {
    [self.loginVC loginWithPhone:nil code:@"3345"];
}

点击按钮测试testExample方法,运行后发现报错如下,意思是可变字典setObject插入对象不能为nil:

caught "NSInvalidArgumentException", 
"*** -[__NSDictionaryM setObject:forKey:]: 
object cannot be nil (key: phone)"

显然loginWithPhone方法对参数没有判断完整.所以实际测试中,可以填入各种类型数据来完善该方法比如null,nil,@"",等等

- (void)testExample {
    [self.loginVC loginWithPhone:nil code:@"3345"];
    [self.loginVC loginWithPhone:null code:nil];
    [self.loginVC loginWithPhone:@"" code:null];
}

(3) 测试项目中文件中的某个方法 - 有返回值

在LoginViewController添加下面校验手机号合法性方法,返回一个bool值:

- (BOOL)checkPhoneStr:(NSString *)phone {
    //判断phone是否合法的代码
    //....

    return YES;
}

然后在测试文件- (void)testExample 中再加上下面两行代码,判断手机是否合法(返回值是否为true,不然报错),然后运行测试, 结果报错,如下图所示:

手机是否合法

(所以现在要做的是传各种参数吧)

二 : 异步功能方法测试 ,通过分析AFNetworking框架描述

AFNetworking涉及多线程和异步等功能,所以拿来学习,下载 AFNetworking 项目,打开项目后直接进入Tests目录下面:

找到AFImageDownloaderTests.m文件,copy前部分代码如下:

#import "AFTestCase.h"
#import "AFImageDownloader.h"

@interface AFImageDownloaderTests : AFTestCase
@property (nonatomic, strong) NSURLRequest *pngRequest;
@property (nonatomic, strong) NSURLRequest *jpegRequest;
@property (nonatomic, strong) AFImageDownloader *downloader;
@end

@implementation AFImageDownloaderTests

- (void)setUp {
    [super setUp];
    self.downloader = [[AFImageDownloader alloc] init];
    [[AFImageDownloader defaultURLCache] removeAllCachedResponses];
    [[[AFImageDownloader defaultInstance] imageCache] removeAllImages];
    self.pngRequest = [NSURLRequest requestWithURL:self.pngURL];
    self.jpegRequest = [NSURLRequest requestWithURL:self.jpegURL];
}

- (void)tearDown {
    [self.downloader.sessionManager invalidateSessionCancelingTasks:YES];
    self.downloader = nil;
    // Put teardown code here. This method is called after the invocation of each test method in the class.
    [super tearDown];
    self.pngRequest = nil;
}

#pragma mark - Image Download

- (void)testThatImageDownloaderSingletonCanBeInitialized {
    AFImageDownloader *downloader = [AFImageDownloader defaultInstance];
    XCTAssertNotNil(downloader, @"Downloader should not be nil");
}

- (void)testThatImageDownloaderCanBeInitializedAndDeinitializedWithActiveDownloads {
    [self.downloader downloadImageForURLRequest:self.pngRequest
                                   success:nil
                                   failure:nil];
    self.downloader = nil;
    XCTAssertNil(self.downloader, @"Downloader should be nil");
}

- (void)testThatImageDownloaderReturnsNilWithInvalidURL
{
    NSMutableURLRequest *mutableURLRequest = [NSMutableURLRequest requestWithURL:self.pngURL];
    [mutableURLRequest setURL:nil];
    /** NSURLRequest nor NSMutableURLRequest can be initialized with a nil URL, 
     *  but NSMutableURLRequest can have its URL set to nil 
     **/
    NSURLRequest *invalidRequest = [mutableURLRequest copy];
    XCTestExpectation *expectation = [self expectationWithDescription:@"Request should fail"];
    AFImageDownloadReceipt *downloadReceipt = [self.downloader
                                               downloadImageForURLRequest:invalidRequest
                                               success:nil
                                               failure:^(NSURLRequest * _Nonnull request, NSHTTPURLResponse * _Nullable response, NSError * _Nonnull error) {
                                                   XCTAssertNotNil(error);
                                                   XCTAssertTrue([error.domain isEqualToString:NSURLErrorDomain]);
                                                   XCTAssertTrue(error.code == NSURLErrorBadURL);
                                                   [expectation fulfill];
                                               }];
    [self waitForExpectationsWithCommonTimeout];
    XCTAssertNil(downloadReceipt, @"downloadReceipt should be nil");
}

简单解释一下上面代码:
1.导入测试文件
2.声明属性
3.setUp方法设置属性,初始化
4.tearDown中销毁属性
5.前两个方法testThatImageDownloaderSingletonCanBeInitialized 和testThatImageDownloaderCanBeInitializedAndDeinitializedWithActiveDownloads :通过XCTAssertNotNil和 XCTAssertNil 判断不为nil 和 为nil. 简单使用
6.下载方法测试:testThatImageDownloaderReturnsNilWithInvalidURL
首先是创建NSMutableURLRequest 和 AFImageDownloadReceipt 对象来下载图片

然后在[self.downloader downloadImageForURLRequest...]方法中block回调进行判断:XCTAssertNotNil和XCTAssertTrue 等等.

关键是下面这行代码:

[expectation fulfill];
fulfill:异步请求结束后需要调用expectation 的 fulfill方法, 通知测试异步请求已结束. 然后执行下面等待超时的方法:

[self waitForExpectationsWithCommonTimeout];
点击该方法,发现跳转到AFTestCase文件中,最后发现执行了下面代码:
[self waitForExpectationsWithTimeout:self.networkTimeout handler:handler];
显然该方法是指多少秒后超时,因为请求是需要时间的,设置Timeout就很有必要了.

所以对于异步执行测试一般以下步骤(OC):
- (void)testExample {
    
    //1: 创建XCTestExpectation对象
    XCTestExpectation* expect = [self expectationWithDescription:@"请求超时timeout!"];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

        sleep(5); //2: 假设请求需要耗时5秒
        NSError *error = [[NSError alloc]init];//3: 假设回调返回一个error
        XCTAssertNotNil(error); //4: 对结果进行判断
        XCTAssertTrue([error.domain isEqualToString:NSURLErrorDomain]);
      
        dispatch_async(dispatch_get_main_queue(), ^{
            //主线程操作....
        });
        [expect fulfill];//5: 异步结束调用fulfill,告知请求结束
        });

    [self waitForExpectationsWithTimeout:15 handler:^(NSError *error) {
        //6: 如果15秒内没有收到fulfill方法通知调用次方法
        //超时后执行一些操作:
        }];
    
    //7: 对象被回收
    XCTAssertNil(expect, @"expect should be nil");
    
}
异步请求单元测试Swift代码:
func testAsyncURLConnection(){
        let URL = NSURL(string: "http://www.baidu.com")!
        let expect = expectation(description: "GET \(URL)")
        
        let session = URLSession.shared
        let task = session.dataTask(with: URL as URL, completionHandler: {(data, response, error) in
            
            XCTAssertNotNil(data, "返回数据不应该为空")
            XCTAssertNil(error, "error应该为nil")
            expect.fulfill() //请求结束通知测试
            
            if response != nil {
                let httpResponse: HTTPURLResponse = response as! HTTPURLResponse
                
                XCTAssertEqual(httpResponse.statusCode, 200, "请求失败!")
                
                DispatchQueue.main.async {
                    //主线程中干事情
                }
                
            } else {
                XCTFail("请求失败!")
            }
        })
        
        task.resume()
        
        //请求超时
        waitForExpectations(timeout: (task.originalRequest?.timeoutInterval)!, handler: {error in
            task.cancel()
        })
    }

三 : 单元测试之Mock使用

使用前需要参考Mock 介绍及下载

Mock是什么?
使用场景:
比如上面(1)异步加载测试:没有网络或者不佳时,自行创建数据. (2)复杂数据库查询:数据库在内网或者暂时无法查询时,自行创建数据.(3)多重网络交互:避免复杂交互,需要简化测试流程,等等才能得到返回数据时.
或者说是,在测试过程中,对于一些不容易构造或不容易获取的对象,此时你可以创建一个虚拟的对象(mock object)来完成测试, Mock却很方便,它直接返回你需要的数据,不用初始化对象,避免复杂的数据获取过程:

如下网站给出的示例代码片段:

- (void)testDisplaysTweetsRetrievedFromConnection
{
  Controller *controller = [[[Controller alloc] init] autorelease];
 //声明id类型对象(不需要TwitterConnection类直接初始化对象)
  id mockConnection = OCMClassMock([TwitterConnection class]);
  controller.connection = mockConnection;

  Tweet *testTweet = /* create a tweet somehow */;   
  NSArray *tweetArray = [NSArray arrayWithObject:testTweet];
 //模拟返回数据
  OCMStub([mockConnection fetchTweets]).andReturn(tweetArray);

  [controller updateTweetView];
}

比如创建tableview测试:

id mockTableView = [OCMockObject mockForClass:[UITableView class]];
    UITableViewCell *cell = [[UITableViewCell alloc] init];
    [[[mockTableView expect] andReturn:cell] dequeueReusableCellWithIdentifier:@"MockTableViewCell" forIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
总而言之,Mock可以方便的创建你想要的object对象,并调用其公共方法.详细Mock语法和使用这里不做介绍

四 : 性能耗时测试

当项目创建完测试文件时,OC就会自动创建下面方法:

- (void)testPerformanceExample {
    // This is an example of a performance test case.
    [self measureBlock:^{
        // Put the code you want to measure the time of here.
    }];
}
这个方法意思是将耗时操作丢到measureBlock里就行了:
[self measureBlock:^{
        // Put the code you want to measure the time of here.
        NSMutableDictionary *dic = @{}.mutableCopy;
        for (NSInteger i = 0; i < 10000; i++) {
            NSString *obj = [NSString stringWithFormat:@"%ld",(long)i];
            [dic setObject:obj forKey:obj];;
        }
    }];

测试后打印日志,其中有平均average: 0.011,所有耗时values: [0.012836, 0.015668, 0.012153, 0.010468, 0.011057, 0.009932, 0.010598, 0.010772, 0.010296, 0.010185],等等:

Test Case '-[ARKit_OCTests testPerformanceExample]' started.
/Users/niexiaobo/Desktop/demo/ARKit-OC/ARKit-OCTests/ARKit_OCTests.m:58: Test Case '-[ARKit_OCTests testPerformanceExample]' 
measured [Time, seconds] average: 0.011, relative standard deviation: 14.609%, 
values: [0.012836, 0.015668, 0.012153, 0.010468, 0.011057, 0.009932, 0.010598, 0.010772, 0.010296, 0.010185], performanceMetricID:com.apple.XCTPerformanceMetric_WallClockTime, baselineName: "", baselineAverage: , maxPercentRegression: 10.000%, maxPercentRelativeStandardDeviation: 10.000%, maxRegression: 0.100, maxStandardDeviation: 0.100
Test Case '-[ARKit_OCTests testPerformanceExample]' passed (0.419 seconds).

传统耗时测试:

NSTimeInterval start = CACurrentMediaTime();
        
        NSMutableDictionary *dic = @{}.mutableCopy;
        for (NSInteger i = 0; i < 10000; i++) {
            NSString *obj = [NSString stringWithFormat:@"%ld",(long)i];
            [dic setObject:obj forKey:obj];;
        }
        NSLog(@"%lf",CACurrentMediaTime() - start);

五 : 单例测试

定义单例,在公共头文件导入宏定义:

#define singleH(name) +(instancetype)share##name;

#if __has_feature(objc_arc)

#define singleM(name) static id _instance;\
+(instancetype)allocWithZone:(struct _NSZone *)zone\
{\
    static dispatch_once_t onceToken;\
    dispatch_once(&onceToken, ^{\
        _instance = [super allocWithZone:zone];\
    });\
    return _instance;\
}\
\
+(instancetype)share##name\
{\
    return [[self alloc]init];\
}\
-(id)copyWithZone:(NSZone *)zone\
{\
    return _instance;\
}\
\
-(id)mutableCopyWithZone:(NSZone *)zone\
{\
    return _instance;\
}
#else
#define singleM static id _instance;\
+(instancetype)allocWithZone:(struct _NSZone *)zone\
{\
static dispatch_once_t onceToken;\
dispatch_once(&onceToken, ^{\
_instance = [super allocWithZone:zone];\
});\
return _instance;\
}\
\
+(instancetype)shareTools\
{\
return [[self alloc]init];\
}\
-(id)copyWithZone:(NSZone *)zone\
{\
return _instance;\
}\
-(id)mutableCopyWithZone:(NSZone *)zone\
{\
return _instance;\
}\
-(oneway void)release\
{\
}\
\
-(instancetype)retain\
{\
    return _instance;\
}\
\
-(NSUInteger)retainCount\
{\
    return MAXFLOAT;\
}
#endif

既然单例的目的是不管怎么初始化创建对象永远都是返回唯一且相同的那个,那么测试也一样,测试不同,重复的方法应该返回同一对象,并且可用:

- (void)testFilesManagerSingle
{
    NSMutableArray *managerArray = [NSMutableArray array];
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        FilesManager *tempManager = [[FilesManager alloc] init];
        [managerArray addObject:tempManager];
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        FilesManager *tempManager = [[FilesManager alloc] init];
        [managerArray addObject:tempManager];
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        FilesManager *tempManager = [FilesManager shareManager];
        [managerArray addObject:tempManager];
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        FilesManager *tempManager = [FilesManager shareManager];
        [managerArray addObject:tempManager];
    });
    
    FilesManager *managerObj = [FilesManager shareManager];
    
    [managerArray enumerateObjectsUsingBlock:^(FilesManager *obj, NSUInteger idx, BOOL * _Nonnull stop) {
        XCTAssertEqual(managerObj, obj, @"FilesManager is not single");
    }];
}

然后测试FilesManager的open和close等方法是否正常.等等

六 : 编写测试用例该注意要点

(1) 要注意创建完成一个测试文件自动创建的几个方法:

- (void)setUp {
    [super setUp];

}
- (void)tearDown {
}

使用:
我们在方法setup()中声明并创建一个Test对象
然后在方法tearDown()中释放它. (有点像init 和 dealloc )

(2) 异步和性能测试往往比较耗时,所以要注意和逻辑测试等分开测试
(3) 测试框架有好几个,对于中小型项目个人觉得考虑兼容性直接使用XCTest
(4) 公用方法等尽量抽离或者写一个宏,比如本节中单例,或者[self waitForExpectationsWithCommonTimeout]; 方法写一个TimeoutTest宏等等.

七 : 封装测试库

当你的测试内容越来越多时,测试代码就像工程一样,甚至更复杂, 同样单元测试也需要封装,继承,设计等等.

比如上面第二节里异步测试,AFImageDownloaderTests测试文件继承自AFTestCase:


AFTestCase

八 : 自动化测试,Jenkins的安装和使用

[编辑中]

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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