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

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

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结尾。你会发现已经有了一个默认的测试用例,其中有四个方法:

#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,它包含了这些测试用例。
测试方法的实现经常包含断言,必须通过验证才能通过测试,举个例子:

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

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

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

#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文件):

#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,然后利用委托实现上面的测试。

iOS开发:XCTest单元测试(附上一个单例的测试代码)的更多相关文章

  1. iOS 用宏定义写一个单例(Singleton)

    用如下方法定义单例 @interface singleton_interface(ClassName); @end 实现单例在 @implemention singleton_implemention ...

  2. IOS 宏定义一个单例

    有时候是不是因为频繁地创建一个单例对象而头疼,一种方式要写好多遍?当然你可以用OC语言进行封装.但下面将介绍一种由C语言进行的封装.只要实现下面的方法,以后建单例对象只要二句话. 1.新建一个.h文件 ...

  3. C++实现一个单例模板类

    单例模式在项目开发中使用得比较多,一个单例的模板类显得很有必要,避免每次都要重复定义一个单例类型 //非多线程模式下的一个单例模板类的实现 // template_singleton.h #inclu ...

  4. iOS常用设计模式:MVC、单例、代理、观察者。

    MVC 模型-视图-控制器(MVC)设计模式 MVC根据角色划分类,涉及到三个角色: Model:模型保存应用程序的数据. View:视图是模型的可视化表示以及用户交互的控件. Controller: ...

  5. struts action不在是一个单例类

    在servlet中,servlet类是一个单例,在servlet中的成员变量,将会被所有请求共享,同时也有可能存在线程安全问题,如有一个成员变量num,每次方法后市的num自增 package act ...

  6. spring中如何向一个单例bean中注入非单例bean

    看到这个题目相信很多小伙伴都是懵懵的,平时我们的做法大都是下面的操作 @Component public class People{ @Autowired private Man man; } 这里如 ...

  7. 用 Java 写一个单例类?

    饿汉式单例 public class Singleton { private Singleton(){} private static Singleton instance = new Singlet ...

  8. 李洪强iOS开发之宏定义方法来初始化一个单例对象

    单例的使用: .m 为了方便实用,只要将以下代码定义在header文件或者.pch文件即可: // .h #define singleton_interface(class) + (instancet ...

  9. iOS开发之单元测试

    开始之前 本文侧重讲述如何在iOS程序的开发过程中使用单元测试.使用Xcode自带的OCUnit作为测试框架. 一.单元测试概述 单元测试作为敏捷开发实践的组成之一,其目的是提高软件开发的效率,维持代 ...

随机推荐

  1. window route 命令

    使用 Route 命令行工具查看并编辑计算机的 IP 路由表.Route 命令和语法如下所示: route [-f] [-p] [Command][Destination] [mask Netmask ...

  2. html里面的相对路径和绝对路径

    HTML有两种路径的写法:相对路径和绝对路径. 相对路径是在同一个目录的文件引用,如果源文件和引用文件在同一个目录里,直接写引用文件名即可. 如: a.html:     c:\test\menu\a ...

  3. [问题2014S12] 复旦高等代数II(13级)每周一题(第十二教学周)

    [问题2014S12]  设 \(A,B\) 都是 \(n\) 阶半正定实对称阵, 证明: \(AB\) 的所有特征值都是非负实数. 进一步, 若 \(A,B\) 都是正定实对称阵, 证明: \(AB ...

  4. java 字符串split有很多坑,使用时请小心!!

    System.out.println(":ab:cd:ef::".split(":").length);//末尾分隔符全部忽略 System.out.print ...

  5. Displaying Window In Center In Oracle Forms 6i

    Center window automatically  in Oracle Forms 6i, use the following procedure by passing window name ...

  6. CentOS 下开启.htaccess

    一般来说,htaccess文件是Apache服务器中的一个配置文件,它负责相关目录下的网页配置.通过htaccess文件,可以帮我们实现: 网页301重定向.自定义404错误页面.改变文件扩展名.允许 ...

  7. FPGA相关术语(一)

    参考资料: 1. 数字时钟管理单元DCM 2. RS-232 知识点: ● Xilinx) Digital Clock Manager(DCM) primitive用于实现延迟锁相环(delay lo ...

  8. 下载SRA文件

    sratoolkit.2.6.2-centos_linux64/bin/prefetch  下载SRA文件 fastq-dump    --split-3    SRR2923014.sra    转 ...

  9. ReactiveCocoa信号使用方法

    最近研究RAC时都是基于UI控件来使用,对单独的signal没有使用过,最近在网上看到一篇文章是关于RAC单独signal的使用.在学习整理后将个人觉得能帮助用于UI控件的一些signal使用方法记录 ...

  10. 判断json数据是否为空

    json数据是没有length这个属性的 ,所以不能直接用.length()方法 我们可以先遍历,然后根据遍历次数求长度 1.在IE上这样遍历json:(js代码) var jsonLength = ...