看到文章标题的时候,你也许会问,测试不是测试妹子干的事吗?的确,测试妹子能帮助我们测试出软件的很多问题(不符合业务的问题),但是代码的测试还得靠我们自己啊。团队Leader在开会时一直强调要打造一支不依靠测试团队的团队,因此,代码自测也变成了一个项目重要的一环。是的,今天我要聊的就是我们程序员对自己代码的测试,而不是测试妹子的测试。在iOS开发中我们用单元测试来保证我们的代码可靠性,什么是单元测试,请看在维基百科上的解释:
在计算机编程中,单元测试(英语:Unit Testing)又称为模块测试, 是针对程序模块的最小单位来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。 -- 维基百科
有了单元测试以后,我们就没必要为了测试某个小模块去编译我们的程序,然后去等待模拟器启动然后到你需要验证的模块去。这样做也是可以的啦!可是,你的项目很大,编译等老半天,你的电脑没那么快,那就够你等的了。我还记得我工作的第一个公司,给我用的是一个性能堪忧的Mac mini,每次修改代码后Command + R
后都得等上一分多,简直不能忍受啊!是的,我没能忍受,没过多久我就离职了。扯淡这么半天,就是为了说明单元测试能节约我们的时间,提高开发效率,对于项目越大的效果越明显。
XCTest
XCode4.x时代Xcode集成的是OCUnit,到了XCode5.x时代就升级为了XCTest,并且到了XCode7时代还有了进行UI测试的能力。除了官方自带的,还有一些比较出名的第三方的测试框架,如:GHUnit
,KiWi
,OCMock
,Specta
等,当然本文不讨论这些第三方框架。怎么知道我们的项目有没有加上单元测试,用Xcode打开你的项目,看文件导航栏有没有类似下图的两个文件夹(TestDemo是工程名)。
其实在我们新建工程的时候就可以为我们的工程选择是否带上单元测试,如下图:
如果你的项目没有上面说的两个文件,你可以通过新建一个Target的方式添加,如下图:
在test下选择你项目没有的便可:
在这2个文件夹目录下分别都有2个文件,一个
.m
文件和一个plist
文件。并且.m
文件有4个方法,如下图:
项目名+Test
的.m
文件里面默认有4个方法,这个文件里面主要做一些逻辑的测试。项目名+UITest
的.m
文件里默认有3个方法。这个文件里面主要做一些UI的测试。说了这么半天,该如何写单元测试呢?在讲解如何写测试方法前,先说说默认的方法是干什么的吧!
//TestDemoTest.m
- (void)setUp {
[super setUp];
//每个test方法执行前调用,在这个测试用例里进行一些通用的初始化工作
// 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];
//每个test方法执行后调用
}
- (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)testPerformanceExample {
//这个方法主要是做性能测试的,所谓性能测试,主要就是评估一段代码的运行时间。该方法就是性能测试方法的样例。
// This is an example of a performance test case.
[self measureBlock:^{
// Put the code you want to measure the time of here.
}];
}
测试用例方法非常简单,从testExample
这个方法我们大概知道怎么写了吧!方法名只需要以test
开头,是的,就是这么简单。现在我们模拟登录这个功能来写一个登录模块的测试用例吧,Demo代码在GitHub,在User
这个模型类里面一个方法叫isChinese
的,是用来判断字符串里面是否有中文的。
#import <Foundation/Foundation.h>
@interface User : NSObject
@property (nonatomic, copy) NSString *userName;
@property (nonatomic, copy) NSString *passWord;
/**
* 判断字符串中是否有中文
*/
- (BOOL)isChinese:(NSString *)string;
@end
现在我们通过Xcode的File
->New
->File
->Source
选择Unit Test Case Class
来新建一个UserTests
,注意要继承XCTestCase
类。
接下来我们为User
类写一个测试isChinese
方法的测试方法,叫做testIsChinese
,测试用例具体如下:
这样,你只要点击测试方法旁边的那个菱形的按钮就可以运行该测试方法啦!通过测试会变成绿色的对勾,失败会变成红色的叉叉。到这里测试用例你就会写了。也许你会在意那些断言,这样的断言有18个,如下:
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没有发生具体异常、具体异常名称的异常时通过测试,反之不通过
UI测试
用代码写UI测试比较麻烦,但是苹果在Xcode中为我们提供了录制的功能。录制是怎么一回事呢?当你打开时这个功能时,测试代码会随着你在设备或模拟器上操作自动创建。这么一来就省事多了。现在,我们在TestDemoUITests.m
文件中写一个方法testLogin
作为测试登录流程操作的UI测试方法。然后把光标放在方法体内,然后点击红色的那个录制按钮,如下:
当你点击了录制后,程序就会自动启动,这时候你在程序的所有操作都会生成想用的代码在你所选择的方法体内。我录制了一个GIF,你可以看一下,非常的好用:
接下来我们看看里面的代码:
//XCUIApplication 这是应用的代理,他能够把你的应用启动起来,并且每次都在一个新进程中。
XCUIApplication *app = [[XCUIApplication alloc] init];
//XCUIElement 这是 UI 元素的代理。元素都有类型和唯一标识。可以结合使用来找到元素在哪里,如当前界面上的一个输入框
XCUIElement *usernameTextField = app.textFields[@"username:"];
[usernameTextField tap];
[usernameTextField typeText:@"xiaofei"];
XCUIElement *passwordTextField = app.textFields[@"password:"];
[passwordTextField tap];
[passwordTextField tap];
[passwordTextField typeText:@"12345"];
[[[[[[[app childrenMatchingType:XCUIElementTypeWindow] elementBoundByIndex:0] childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element tap];
[app.buttons[@"login"] tap];
有了这些代码,我们就可以对它进行一些处理了,比如:
//XCUIApplication 这是应用的代理,他能够把你的应用启动起来,并且每次都在一个新进程中。
XCUIApplication *app = [[XCUIApplication alloc] init];
//XCUIElement 这是 UI 元素的代理。元素都有类型和唯一标识。可以结合使用来找到元素在哪里,如当前界面上的一个输入框
XCUIElement *usernameTextField = app.textFields[@"username:"];
[usernameTextField tap];
[usernameTextField typeText:@"xiaofei"];
XCUIElement *passwordTextField = app.textFields[@"password:"];
[passwordTextField tap];
[passwordTextField tap];
[passwordTextField typeText:@"12345"];
[[[[[[[app childrenMatchingType:XCUIElementTypeWindow] elementBoundByIndex:0] childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element tap];
[app.buttons[@"login"] tap];
//登录成功后的控制器的title为loginSuccess,只需判断控制器的title时候一样便可判断登录是否成功
XCTAssertEqualObjects(app.navigationBars.element.identifier, @"loginSuccess");
如果你想一次跑完所有的测试方法,快捷键cmd
+u
即可。跑起来后的面板主要如下图所示:
总结
这只是苹果官方集成在Xcode中的简单框架,优点就是简单,缺点也是简单。当然它的用法也绝非如此,有很多还待开发。苹果官方也有一个Demo,地址点击这里
网友评论
[app.otherElements[@"\\U6211\\U7684"] tap];
这中间其实只有一个"\",我因为编译不通过特别转义了一下,但是运行的时候告诉我用户界面测试失败,没有找到匹配,这个问题该怎么去解决啊