当前位置: 首页 > news >正文

在XCode中使用XCTest

测试驱动开发并不是一个很新鲜的概念了。在我最开始学习程序编写时,最喜欢干的事情就是编写一段代码,然后运行观察结果是否正确。我所学习第一门语言是c语言,用的最多的是在算法设计上,那时候最常做的事情就是编写了一段代码,如何编译运行,查看结果是否正确,很多时候,还得自己想很多特殊的(比如说零值,边界值)测试数据来检测所写代码、算法是否正确。那个时候,感觉还好,比较输出只是只是控制台的一个简单的数字或者字符。在学习iOS开发中,很多时候也是要测试的,这种输出是必须在点击一系列按钮之后才能在屏幕上显示出来的东西。测试的时候,往往是用模拟器一次一次的从头开始启动app,然后定位到自己所在模块的程序,做一系列的点击操作,然后查看结果是否符合自己预期。

这种行为无疑是对美好生命和绚丽青春的巨大浪费。于是有很多资深工程师们发现,我们是可以在代码中构造一个类似的场景,然后在代码中调用我们之前想要检查的代码,并将运行结果和设想结果在程序中进行比较,如果一致,则说明我们的代码没有问题。比如说下面的代码:

1
2
3
4
5
6
7
8
9
10
int  a = 3, b = 4;
 
int  c = a + b;
 
if  (c == a + b){
     //结果正确
}
else {
    //结果错误

 当测试足够全面、具有代表性的时候,我们就可以肯定这个代码是没有问题的,至少,问题不是出自这块代码。我们做出某些条件和假设,并以其为条件使用到被测试中的代码去,比较预期结果与运行结果是否相等,这就是软件测试中的基本方法。

 

首先什么是单元测试?维基百科中的解释是:

在计算机编程中,单元测试(又称为模块测试, Unit Testing)是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。

通常来说,程序员每修改一次程序就会进行最少一次单元测试,在编写程序的过程中前后很可能要进行多次单元测试,以证实程序达到软件规格书(en:Specification)要求的工作目标,没有程序错误;虽然单元测试不是什么必须的,但也不坏,这牵涉到项目管理的政策决定。

在XCode中使用XCTest

在XCode7中新建一个工程的时候,会默认带一个用于单元测试的target,其名字为工程名加Test后缀,并且文件名也以Test结尾。你会发现已经有了一个默认的测试用例

 注意到画勾的地方,Include Unit Test就是包含单元测试的意思。打开工厂目录,你会发现有如下文件:

 其中,ZYMusicPlayerTests文件夹目录下的文件就是我们的单元测试文件。

新建一个工程的时候,会默认带一个用于单元测试的target,其名字为工程名加Tests后缀,并且文件名也以Test结尾。你会发现已经有了一个默认的测试用例,其中有四个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#import <XCTest/XCTest.h>
 
@interface  ZYMusicPlayerTests : XCTestCase
 
@end
 
@implementation  ZYMusicPlayerTests
 
- ( 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.
//    XCTFail(@"no implementation for app",__PRETTY_FUNCTION__);
}
 
- ( void )testPerformanceExample {
     // This is an example of a performance test case.
     [ self  measureBlock:^{
     }];
}
 
 
@end

 

四个方法分别是:setUp, tearDown, testExample, testPerformanceExample。其中testExample方法左侧有一个播放按钮,点击它就会对这个方法进行测试,而在整个文件的@implemenation那行也有个同样的按钮,点击后会对当前测试用例的所有方法进行测试,也可通过Command+U快捷键来触发。这个测试用例类没有头文件,因为测试用例不需要给外部暴漏接口。按照苹果官方的文档,建立一个测试用例的过程应该是这样的:

    1. 建立一个XCTestCase的子类
    2. 实现测试方法
    3. 选择性的定义一些实例变量来存储fixture的状态
    4. 通过重写setUp方法选择性的实例化fixture
    5. 通过重写tearDown方法来在测试后清除
      测试方法没有参数和返回值,用test作为前缀,比如:

      - (void)testPlayingMusic

 

会自动被XCTest架构识别为测试用例,每个XCTestCase的子类中的defaultTestSuite都是一个XCTestSuite,它包含了这些测试用例。
测试方法的实现经常包含断言,必须通过验证才能通过测试,举个例子:

 

 下面是使用时的所有断言测试:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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 。例如

 参考自:http://yulingtianxia.com/blog/2014/04/28/iosdan-yuan-ce-shi-xctest/

有一部分已亲测。

在使用XCTest的时候,一般是所需要测试的那个类的类名+Tests,如ZYAudioManagerTests就是为了测试ZYAudioManager类。并且这个类要继承自XCTestCase类,或者它的子类,如:

@interface ZYAudioManagerTests : XCTestCase

 

下面是一个音乐播放的单例代码与它的测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>
@interface  ZYAudioManager :  NSObject
+ (instancetype)defaultManager;
 
//播放音乐
- (AVAudioPlayer *)playingMusic:( NSString  *)filename;
- ( void )pauseMusic:( NSString  *)filename;
- ( void )stopMusic:( NSString  *)filename;
 
//播放音效
- ( void )playSound:( NSString  *)filename;
- ( void )disposeSound:( NSString  *)filename;
@end
 
 
 
#import "ZYAudioManager.h"
 
@interface  ZYAudioManager ()
@property  ( nonatomic , strong)  NSMutableDictionary  *musicPlayers;
@property  ( nonatomic , strong)  NSMutableDictionary  *soundIDs;
@end
 
static  ZYAudioManager *_instance =  nil ;
 
@implementation  ZYAudioManager
+ (instancetype)defaultManager
{
     static  dispatch_once_t onceToken;
     dispatch_once(&onceToken, ^{
         _instance = [[ self  alloc] init];
     });
     return  _instance;
}
 
- (instancetype)init
{
     __block ZYAudioManager *temp =  self ;
     
     static  dispatch_once_t onceToken;
     dispatch_once(&onceToken, ^{
         if  ((temp = [ super  init]) !=  nil ) {
             _musicPlayers = [ NSMutableDictionary  dictionary];
             _soundIDs = [ NSMutableDictionary  dictionary];
         }
     });
     self  = temp;
     return  self ;
}
 
+ (instancetype)allocWithZone:( struct  _NSZone *)zone
{
     static  dispatch_once_t onceToken;
     dispatch_once(&onceToken, ^{
         _instance = [ super  allocWithZone:zone];
     });
     return  _instance;
}
 
//播放音乐
- (AVAudioPlayer *)playingMusic:( NSString  *)filename
{
     if  (filename ==  nil  || filename.length == 0)   return  nil ;
     
     AVAudioPlayer *player =  self .musicPlayers[filename];       //先查询对象是否缓存了
     
     if  (!player) {
         NSURL  *url = [[ NSBundle  mainBundle] URLForResource:filename withExtension: nil ];
         
         if  (!url)   return  nil ;
         
         player = [[AVAudioPlayer alloc] initWithContentsOfURL:url error: nil ];
         
         if  (![player prepareToPlay])  return  nil ;
         
         self .musicPlayers[filename] = player;             //对象是最新创建的,那么对它进行一次缓存
     }
     
     if  (![player isPlaying]) {                  //如果没有正在播放,那么开始播放,如果正在播放,那么不需要改变什么
         [player play];
     }
     return  player;
}
 
- ( void )pauseMusic:( NSString  *)filename
{
     if  (filename ==  nil  || filename.length == 0)   return ;
     
     AVAudioPlayer *player =  self .musicPlayers[filename];
     
     if  ([player isPlaying]) {
         [player pause];
     }
}
- ( void )stopMusic:( NSString  *)filename
{
     if  (filename ==  nil  || filename.length == 0)   return ;
     
     AVAudioPlayer *player =  self .musicPlayers[filename];
     
     [player stop];
     
     [ self .musicPlayers removeObjectForKey:filename];
}
 
//播放音效
- ( void )playSound:( NSString  *)filename
{
     if  (!filename)  return ;
     
     //取出对应的音效ID
     SystemSoundID soundID = ( int )[ self .soundIDs[filename] unsignedLongValue];
     
     if  (!soundID) {
         NSURL  *url = [[ NSBundle  mainBundle] URLForResource:filename withExtension: nil ];
         if  (!url)  return ;
         
         AudioServicesCreateSystemSoundID((__bridge CFURLRef)(url), &soundID);
         
         self .soundIDs[filename] = @(soundID);
     }
     
     // 播放
     AudioServicesPlaySystemSound(soundID);
}
 
//摧毁音效
- ( void )disposeSound:( NSString  *)filename
{
     if  (!filename)  return ;
     
     
     SystemSoundID soundID = ( int )[ self .soundIDs[filename] unsignedLongValue];
     
     if  (soundID) {
         AudioServicesDisposeSystemSoundID(soundID);
         
         [ self .soundIDs removeObjectForKey:filename];     //音效被摧毁,那么对应的对象应该从缓存中移除
     }
}
@end

 测试代码(测试代码,只有.m文件,无.h文件):

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
#import <XCTest/XCTest.h>
#import "ZYAudioManager.h"
#import <AVFoundation/AVFoundation.h>
 
@interface  ZYAudioManagerTests : XCTestCase
@property  ( nonatomic , strong) AVAudioPlayer *player;
@end
static  NSString  *_fileName = @ "10405520.mp3" ;
@implementation  ZYAudioManagerTests
 
- ( 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 )testAudioManagerSingle
{
     NSMutableArray  *managers = [ NSMutableArray  array];
     
     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
         ZYAudioManager *tempManager = [[ZYAudioManager alloc] init];
         [managers addObject:tempManager];
     });
     
     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
         ZYAudioManager *tempManager = [[ZYAudioManager alloc] init];
         [managers addObject:tempManager];
     });
     
     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
         ZYAudioManager *tempManager = [ZYAudioManager defaultManager];
         [managers addObject:tempManager];
     });
     
     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
         ZYAudioManager *tempManager = [ZYAudioManager defaultManager];
         [managers addObject:tempManager];
     });
     
     ZYAudioManager *managerOne = [ZYAudioManager defaultManager];
     
     [managers enumerateObjectsUsingBlock:^(ZYAudioManager *obj,  NSUInteger  idx,  BOOL  * _Nonnull stop) {
         XCTAssertEqual(managerOne, obj, @ "ZYAudioManager is not single" );
     }];
}
 
/**
  *  测试是否可以正常播放音乐
  */
- ( void )testPlayingMusic
{
     self .player = [[ZYAudioManager defaultManager] playingMusic:_fileName];
     XCTAssertTrue( self .player.playing, @ "ZYAudioManager is not PlayingMusic" );
}
 
/**
  *  测试是否可以正常停止音乐
  */
- ( void )testStopMusic
{
     if  ( self .player ==  nil ) {
         self .player = [[ZYAudioManager defaultManager] playingMusic:_fileName];
     }
     
     if  ( self .player.playing ==  NO ) [ self .player play];
     
     [[ZYAudioManager defaultManager] stopMusic:_fileName];
     XCTAssertFalse( self .player.playing, @ "ZYAudioManager is not StopMusic" );
}
 
/**
  *  测试是否可以正常暂停音乐
  */
- ( void )testPauseMusic
{
     if  ( self .player ==  nil ) {
         self .player = [[ZYAudioManager defaultManager] playingMusic:_fileName];
     }
     if  ( self .player.playing ==  NO ) [ self .player play];
     [[ZYAudioManager defaultManager] pauseMusic:_fileName];
     XCTAssertFalse( self .player.playing, @ "ZYAudioManager is not pauseMusic" );
}
 
@end

 Command + U运行测试。

 

我应该测试什么?

学习完上面的使用方法时,迷茫了,在具体开发中,如果写测试代码,那么我该测试什么?私有方法也需要测试么?

我们不需要去测试私有方法,除此之外要回答“我该测试什么?”这个问题,并没有这么简单,但我依旧希望测试代码可以按照我实际编码时候的想法去测试,那么就是测试就仅仅是调用了我的共有方法。

- (void)testDownloadData;

 

像这样的测试有一个根本的问题:它不会告诉你应该发生什么,也就是不会告诉你实际的预期是什么。它不清楚需求是什么。

应该测试什么?我不应该关注于测试,而应该关注行为,应该测试行为。

什么是行为?

让我们思考你设计的 app 中的一个对象。它有一个接口定义了其方法和依赖关系。这些方法和依赖,声明了你对象的约定。它们定义了如何与你应用的其他部分交互,以及它的功能是什么。它们定义了对象的行为。

 有很多行为,或许是私有的。比如说,我想测试一个继承自UIViewController的ZYNewViewController里面的tableView的dataSource的三个必须实现的数据源方法是否实现?

以往写代码,根据封装的原则,tableView必然是私有的,那么难道为了方便测试,我们就应该将它写成public?

不,有一种可以解决的方案是,写一个公共的TestsProcotol,然后利用委托实现上面的测试。

 

转载于:https://www.cnblogs.com/W-Kr/p/5221886.html

相关文章:

  • wordpress在IIS下无rewrite利用cos-html-cache实现静态页面
  • java基础知识系列---内部类
  • 32BPP窗口模式下24位位图的像素操作(2)
  • 帮你的WordPress博客添加主页、文章页的关键字和描述
  • 域名带www与不带www重定向问题
  • 引用和指针
  • 301重定向www域名
  • sass入门
  • 实现一个均匀Tile的卷轴显示引擎
  • linux rpm包解压
  • 教你彻底实现红黑树:红黑树的c源码实现与剖析
  • 【one day one linux】好用的数据处理工具awk
  • 新年新起点-荣获2011年度Silverlight方向Microsoft® MVP奖
  • java,关于POJO,查阅资料
  • 阅读计划
  • Git同步原始仓库到Fork仓库中
  • Java基本数据类型之Number
  • JS变量作用域
  • Linux CTF 逆向入门
  • Node + FFmpeg 实现Canvas动画导出视频
  • opencv python Meanshift 和 Camshift
  • Python_网络编程
  • webpack+react项目初体验——记录我的webpack环境配置
  • webpack项目中使用grunt监听文件变动自动打包编译
  • 持续集成与持续部署宝典Part 2:创建持续集成流水线
  • 从零开始学习部署
  • 理解 C# 泛型接口中的协变与逆变(抗变)
  • 面试遇到的一些题
  • 排序(1):冒泡排序
  • 前端攻城师
  • 腾讯大梁:DevOps最后一棒,有效构建海量运营的持续反馈能力
  • 线上 python http server profile 实践
  • nb
  • Linux权限管理(week1_day5)--技术流ken
  • postgresql行列转换函数
  • Salesforce和SAP Netweaver里数据库表的元数据设计
  • ​LeetCode解法汇总2696. 删除子串后的字符串最小长度
  • ​软考-高级-信息系统项目管理师教程 第四版【第14章-项目沟通管理-思维导图】​
  • ​虚拟化系列介绍(十)
  • (附源码)计算机毕业设计大学生兼职系统
  • (三)uboot源码分析
  • (四)c52学习之旅-流水LED灯
  • (转)微软牛津计划介绍——屌爆了的自然数据处理解决方案(人脸/语音识别,计算机视觉与语言理解)...
  • (转)原始图像数据和PDF中的图像数据
  • (轉)JSON.stringify 语法实例讲解
  • .bat批处理(五):遍历指定目录下资源文件并更新
  • .Net Core和.Net Standard直观理解
  • .net 受管制代码
  • .net 微服务 服务保护 自动重试 Polly
  • .net 无限分类
  • .Net6使用WebSocket与前端进行通信
  • .vimrc php,修改home目录下的.vimrc文件,vim配置php高亮显示
  • ;号自动换行
  • @javax.ws.rs Webservice注解
  • @Service注解让spring找到你的Service bean