iOS开发:XCTest单元测试(附上一个单例的测试代码)

测试驱动开发并不是一个很新鲜的概念了。在我最开始学习程序编写时,最喜欢干的事情就是编写一段代码,然后运行观察结果是否正确。我所学习第一门语言是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,然后利用委托实现上面的测试。

时间: 2024-10-09 21:59:12

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

IOS 宏定义一个单例

有时候是不是因为频繁地创建一个单例对象而头疼,一种方式要写好多遍?当然你可以用OC语言进行封装.但下面将介绍一种由C语言进行的封装.只要实现下面的方法,以后建单例对象只要二句话. 1.新建一个.h文件,在文件中实现以下方法: 1 / .h 2 #define singleton_interface(class) + (instancetype)shared##class; 3 4 // .m 5 #define singleton_implementation(class) 6 static c

iOS工作记录9:项目单例(直接拉用)

我就直接用个例子记录: 工程代码记录下来: // //  Ticket.h #import <Foundation/Foundation.h> @interface Ticket : NSObject // 实例化票据的单例 + (Ticket *)sharedTicket; // 在多线程应用中,所有被抢夺资源的属性需要设置为原子属性 // 系统会在多线程抢夺时,保证该属性有且仅有一个线程能够访问 // 注意:使用atomic属性,会降低系统性能,在开发多线程应用时,尽量不要资源 // 另外

创建一个单例类

关于单例的概念此处不做表述,直接上代码演示如何创建一个单例类. 1 #import <Foundation/Foundation.h> 2 3 @interface MJDemo : NSObject 4 5 + (instancetype)sharedDemo; 6 7 @end 8 9 10 #import "MJDemo.h" 11 12 @implementation MJDemo 13 14 //在iOS中所有对象分配内存空间,最终都会调用allocWithZon

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

MVC 模型-视图-控制器(MVC)设计模式 MVC根据角色划分类,涉及到三个角色: Model:模型保存应用程序的数据. View:视图是模型的可视化表示以及用户交互的控件. Controller:控制器是一个协调所有工作的中介者.它访问模型中的数据并在视图中展示它们,同时它们还监听事件和操作数据. 单例设计模式 单例设计模式可以确保对于一个类只有一个实例(对象)存在,这个对象有一个全局的访问点.通常采用懒加载的方式在第一次用到对象的时候才去创建它. 如何创建一个单例: 如:在SortDeta

iOS开发UI篇—实现一个私人通讯录小应用(二)

iOS开发UI篇—实现一个私人通讯录小应用(二) 一.实现功能说明 (1)点击注销按钮,弹出一个对话框,点击确定后移除当前栈顶的控制器,返回开始界面,点击取消,不做任何操作. 注意:注销按钮的单击事件已经进行了连线.实现-(void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex需要遵守UIActionSheetDelegate协议. 1 //注销按钮 2 - (IBActi

iOS开发UI篇—实现一个私人通讯录小应用(一)

iOS开发UI篇—实现一个私人通讯录小应用(一) 一.该部分主要完成内容 1.界面搭建                        2.功能说明 (1).只有当账号和密码输入框都有值的时候,登录按钮才能交互 (2).当取消勾选记住密码后,自动登录按钮也随之取消:当勾选了自动登录按钮时,记住密码按钮也一同勾选. (3).点击登陆后,弹出蒙版,界面不可交互,程序能够简单判断账号和密码是否正确,如果不正确则给出相应的提示,如果正确则跳转到联系人列表界面. 二.实现过程和代码 项目文件结构图和界面搭建

当我使用一个单例,调用一个类型对象的时候,程序出现了崩溃

具体报错如下,希望可以得到解决: 2014-06-28 08:57:46.508 XYZ[707:1403] *** Assertion failure in -[UIKeyboardTaskQueue waitUntilAllTasksAreFinished], /SourceCache/UIKit_Sim/UIKit-2935.137/Keyboard/UIKeyboardTaskQueue.m:368 2014-06-28 08:57:46.509 XYZ[707:1403] *** Te

struts action不在是一个单例类

在servlet中,servlet类是一个单例,在servlet中的成员变量,将会被所有请求共享,同时也有可能存在线程安全问题,如有一个成员变量num,每次方法后市的num自增 1 package action; 2 3 import java.io.IOException; 4 import javax.servlet.ServletException; 5 import javax.servlet.http.HttpServlet; 6 import javax.servlet.http.H

iOS开发UI篇—实现一个简单的手势解锁应用(基本)

iOS开发UI篇—实现一个简单的手势解锁应用(基本) 一.实现效果 实现效果图: 二.手势解锁应用分析 1.监听手指在view上的移动,首先肯定需要自定义一个view,重写touch began,touch move等方法,当手指移动到圈上时,让其变亮.可以通过button按钮来实现. 2.界面搭建 背景图片(给控制器的view添加一个imageview,设置属性背景图片) 九个按钮(把九个按钮作为一个整体,使用一个大的view来管理这些小的view,这些小的view就是9个button.如果使