在iOS开发中,很多时候也是要测试的,这种输出是必须在点击一系列按钮之后才能在屏幕上显示出来的东西。测试的时候,往往是用模拟器一次一次的从头开始启动app,然后定位到自己所在模块的程序,做一系列的点击操作,然后查看结果是否符合自己预期。这种行为无疑是对美好生命和绚丽青春的巨大浪费。于是有很多资深工程师们发现,我们是可以在代码中构造一个类似的场景,然后在代码中调用我们之前想要检查的代码,并将运行结果和设想结果在程序中进行比较,如果一致,则说明我们的代码没有问题。
在XCode7中新建一个工程的时候,会默认带一个用于单元测试的target,其名字为工程名加Test后缀,并且文件名也以Test结尾。
你会发现已经有了一个默认的测试用例,其中有四个方法:setUp, tearDown, testExample, testPerformanceExample。其中testExample方法左侧有一个播放按钮,点击它就会对这个方法进行测试,而在整个文件的@implemenation那行也有个同样的按钮,点击后会对当前测试用例的所有方法进行测试,也可通过Command+U快捷键来触发。这个测试用例类没有头文件,因为测试用例不需要给外部暴漏接口。
按照苹果官方的文档,建立一个测试用例的过程应该是这样的:
建立一个XCTestCase的子类
实现测试方法
选择性的定义一些实例变量来存储fixture的状态
通过重写setUp方法选择性的实例化fixture
通过重写tearDown方法来在测试后清除
测试方法没有参数和返回值,用test作为前缀,比如:
- (void)testPlayingMusic
下面是使用时的所有断言测试:
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。
下面是一个音乐播放的单例代码与它的测试代码:
#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运行测试。