原文地址:
https://www.jianshu.com/p/4001e06b150e
前言:
对于单元测试来说,我想大部分同行,在项目中,很少会用到,也有一大部分,知道单元测试这个东西,但是确切的说没有尝试过,也不知道怎么回事,我想写篇文章总结一下,了解一下单元测试。我也志在学习一下单元测试。如果触碰到什么误区,希望大家多多提醒,帮助,谢谢。
我看了几篇单元测试的文章,其中写到单元测试多数用于:
1.调试接口是否正常使用。比如要测试一个网络接口,通常每次都要重新启动,经过繁复的操作之后,才能测试到网络接口。要是用单元测试,就可以直接测试那个方法,相对方便很多。
2.比如由于修改较多,想测试分享功能是否正常,(而不是重新启动程序,进入到分享界面,点击分享,填写分享内容。),在单元测试通过了,直接用到相应的地方。
3.自动发布、自动测试(特别在一些大的项目,以防止程序被误改或引起新的问题)。
4.用户注册/登陆等
了解一下单元测试:
单元测试(Unit Testing)又称为模块测试,是针对程序模块软件设计来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。对于面向对象编程,最小单元就是方法,包括基类、抽象类、或者派生类中的方法。
通常来说,程序员每修改一次代码就会修改某个单元,那我们就可以对这个单元做修改的验证(单元测试),在编写程序的过程中前后很可能要进行多次单元测试,以证实程序达到软件规格书(产品需求)要求的工作目标,而且没有程序错误。
每个理想的测试案例独立于其它case,测试时需隔离模块。单元测试通常由软件开发人员编写,用于确保所写的代码匹配软件需求和遵循开发目标。它的实施方式可以是手动的,或是构建自动化的一部分。
单元测试允许程序员在未来重构代码,且确保模块依然工作正确。这个过程是为所有方法编写单元测试,一旦变更导致错误发生,借助于单元测试可以快速定位并修复错误。
可读性强的单元测试可以使程序员方便地检查代码片断是否依然正常工作。良好设计的单元测试案例覆盖程序单元分支和循环条件的所有路径。在连续的单元测试环境,通过其固有的持续维护工作,单元测试可以延续用于准确反映当任何变更发生时可执行程序和代码的表现。借助于上述开发实践和单元测试的覆盖,可以总是维持准确性。
了解一下单元测试目的:
保证代码的质量 (帮助你编写高质量代码、减少bu)
代码可以通过编译器检查语法的正确性,却不能保证代码逻辑是正确的,尤其包含了许多单元分支的情况下,单元测试可以保证代码的行为和结果与我们的预期和需求一致。在测试某段代码的行为是否和你的期望一致时,你需要确认,在任何情况下,这段代码是否都和你的期望一致,譬如参数可能为空,可能的异步操作等。
有一部分bug的原因是开发人员在编写工作代码的时候没有考虑到某些case或者边际条件。造成这种问题的原因很多,其中很重要的一个原因是我们对工作代码所要完成的功能思考不足,而编写单元测试,特别是先写单元测试再写工作代码就可以帮助开发人员思考编写的代码到底要实现哪些功能。例如实现一个简单的用户注册功能的业务类方法,用单元测试再写工作代码的方式来工作的话开发人员就会先考虑各种场景相关,例如正常注册、用户名重复、没有满足必要的填写内容......等等,之后就会编写相关的测试用例。编写单元测试代码的过程就是促使开发人员思考工作代码实现内容和逻辑的过程,之后实现工作代码的时候,开发人员思路会更清晰,实现代码的质量也会有相应的提升。
保证代码的可维护性 (提升代码的反馈速度,减少重复工作,保证你最后的代码修改不会破坏之前代码的功能)
保证原有单元测试正确的情况下,无论如何修改单元内部代码,测试的结果应该是正确的,且修改后不会影响到其他的模块。
开发人员实现某个功能或者修补了某个bug,如果有相应的单元测试支持的话,开发人员可以马上通过运行单元测试来验证之前完成的代码是否正确,而不需要反复通过编译运行simulator、等待应用启动、通过输入数据等繁琐的步骤来验证所完成的功能。用单元测试代码来验证代码和通过发布应用以人工的方式来验证代码这两者的效率差很多,所以单元测试其实还能节约人力成本。
项目越做越大,代码越来越多,特别涉及到一些公用接口之类的代码或是底层的基础库,谁也不敢保证这次修改的代码不会破坏之前的功能,所以与此相关的需求会被搁置或推迟,由于不敢改进代码,代码也变得越来越难以维护,质量也越来越差。而单元测试就是解决这种问题的很好方法(不敢说最好的)。由于代码的历史功能都有相应的单元测试保证,修改了某些代码以后,通过运行相关的单元测试就可以验证出新调整的功能是否有影响到之前的功能。当然要实现到这种程度需要很大的付出,不但要能够达到比较高的测试覆盖率,而且单元测试代码的编写质量也要有保证。
保证代码的可扩展性
为了保证可行的可持续的单元测试,程序单元应该是低耦合的,否则,单元测试将难以进行,说明代码的依赖性很高。
了解一下单元测试的本质:
是一种验证行为
单元测试在开发前期检验了代码逻辑的正确性,开发后期,无论是修改代码内部抑或重构,测试的结果为这一切提供了可量化的保障。
是一种设计行为
为了可进行单元测试,尤其是先写单元测试(TDD),我们将从调用者思考,从接口上思考,我们必须把程序单元设计成接口功能划分清晰的,易于测试的,且与外部模块耦合性尽可能小。
是一种快速回归的方式
在原代码基础上开发及修改功能时,单元测试是一种快捷,可靠的回归。
除了那些大拿们编写的代码,我相信很多易于维护、设计良好的代码都是通过不断的重构才得到的。虽然说单元测试本身不能直接改进生产代码的质量,但它为生产代码提供了“安全网”,让开发人员可以勇敢地改进代码,从而让代码的clean和beautiful不再是梦想。
是程序优良的文档
从效果上而言,单元测试就像是能执行的文档,说明了在你用各种条件调用代码时,你所能期望这段代码完成的功能。
由于给代码写很多单元测试,相当于给代码加上了规格说明书,开发人员通过读单元测试代码也能够帮助开发人员理解现有代码。很有Open Source的项目(如,AFNetworking, FMDB,喵神的VVDoucment等)都有相当量的单元测试代码,通过读这些测试代码会有助于理解生产源代码。
两种测试思想
测试驱动开发(Test-driven development,TDD)是一种软件开发过程中的应用方法,由极限编程中倡导,以其倡导先写测试程序,然后编码实现其功能得名。测试驱动开发是戴两顶帽子思考的开发方式:先戴上实现功能的帽子,在测试的辅助下,快速实现其功能;再戴上重构的帽子,在测试的保护下,通过去除冗余的代码,提高代码质量。测试驱动着整个开发过程:首先,驱动代码的设计和功能的实现;其后,驱动代码的再设计和重构。
行为驱动开发(Behavior-driven development,BDD)是一种敏捷软件开发的技术,BDD的重点是通过与利益相关者的讨论取得对预期的软件行为的清醒认识。它通过用自然语言书写非程序员可读的测试用例扩展了 测试驱动开发方法(TDD)。这让开发者得以把精力集中在代码应该怎么写,而不是技术细节上,而且也最大程度的减少了将代码编写者的技术语言与商业客户、用户、利益相关者、项目管理者等的领域语言之间来回翻译的代价。
在iOS单元测试框架中,kiwi是BDD的代表。
介绍
OCUnit(即用XCTest进行测试)其实就是苹果自带的测试框架。
GHUnit是一个可视化的测试框架。
有了它,你可以点击APP来决定测试哪个方法,并且可以点击查看测试结果等。
OCMock就是模拟某个方法或者属性的返回值,你可能会疑惑为什么要这样做?使用用模型生成的模型对象,再传进去不就可以了?答案是可以的,但是有特殊的情况。比如你测试的是方法A,方法A里面调用到了方法B,而且方法B是有参数传入,但又不是方法A所提供。这时候,你可以使用OCMock来模拟方法B返回的值。(在不影响测试的情况下,就可以这样去模拟。)除了这些,在没有网络的情况下,也可以通过OCMock模拟返回的数据。
UITests就是通过代码化来实现自动点击界面,输入文字等功能。靠人工操作的方式来覆盖所有测试用例是非常困难的,尤其是加入新功能以后,旧的功能也要重新测试一遍,这导致了测试需要花非常多的时间来进行回归测试,这里产生了大量重复的工作,而这些重复的工作有些是可以自动完成的,这时候UITests就可以帮助解决这个问题了。
案例 1
简单的单元测试
1-1 创建一个新的项目
Snip20180619_12.png
1-2点开测试文件,进入到这个类
setUp :每个测试方法调用前执行tearDown :每个测试方法调用后执行testExample :是测试方法,和我们新建的没有差别。 测试方法必须testXXX的格式,且不能有参数,不然不会识别为测试方法 测试方法的执行顺序: 字典序排序。快捷键:Command + U进行单元测试,这个快捷键是全部测试。
image.png
1-3在testExample方法中输入如下:
NSLog(@"自定义测试testExample");inta=3;XCTAssertTrue(a==0,"a 不能等于 0");
备注:红色的叉子:代表测试未通过。绿色叉子:代表测试通过。
案例 2
案例 3
进行网络请求的测试
使用CocoaPods安装AFNetworking和STAlertView(CocoaPods安装和使用教程)
Pofile:
platform:ios,'7.0'target'UnitTestDemoTests'dopod'AFNetworking','~> 2.5.0'pod'STAlertView','~> 1.0.0'endtarget'UnitTestDemoTestsTests'dopod'AFNetworking','~> 2.5.0'pod'STAlertView','~> 1.0.0'end
iOS9的http安全问题:现在进行异步请求的网络测试,由于测试方法主线程执行完就会结束,所以需要设置一下,否则没法查看异步返回结果。
也可以在方法结束前设置等待,调回回来的时候再让它继续执行。(另一种异步函数的单元测试)定义宏如下:
//waitForExpectationsWithTimeout是等待时间,超过了就不再等待往下执行。#defineWAIT do {\[self expectationForNotification:@"RSBaseTest" object:nil handler:nil];\[self waitForExpectationsWithTimeout:30 handler:nil];\} while (0);#defineNOTIFY \[[NSNotificationCenter defaultCenter]postNotificationName:@"RSBaseTest" object:nil];
增加测试方法:
-(void)testRequest{// 1.获得请求管理者AFHTTPRequestOperationManager*mgr=[AFHTTPRequestOperationManager manager];mgr.responseSerializer.acceptableContentTypes=[NSSet setWithObjects:@"text/html",nil];// 2.发送GET请求[mgr GET:@"http://www.weather.com.cn/adat/sk/101110101.html"parameters:nil success:^(AFHTTPRequestOperation*operation,id responseObject){NSLog(@"responseObject:%@",responseObject);XCTAssertNotNil(responseObject,@"返回出错");NOTIFY//继续执行}failure:^(AFHTTPRequestOperation*operation,NSError*error){NSLog(@"error:%@",error);XCTAssertNil(error,@"请求出错");NOTIFY//继续执行}];WAIT//暂停}
有时候我们想测试一下整个流程是否可以跑通,比如获取验证码、登录、上传头像,查询个人资料。其实只要输入验证码就可以完成整个测试。这时候就需要用到输入框了,以便程序继续执行。使用了一个第三方的弹出输入框STAlertView,前面已经设置。
STAlertView的使用方法:
-(void)testAlertView{self.stAlertView=[[STAlertView alloc]initWithTitle:@"验证码"message:nil textFieldHint:@"请输入手机验证码"textFieldValue:nil cancelButtonTitle:@"取消"otherButtonTitle:@"确定"cancelButtonBlock:^{//点击取消返回后执行[selftestAlertViewCancel];NOTIFY//继续执行}otherButtonBlock:^(NSString*b){//点击确定后执行[selfalertViewComfirm:b];NOTIFY//继续执行}];[self.stAlertView show];WAIT//设置等待时间}
案例 4
测试的执行顺序
Snip20180619_1.png
通过上述测试得出结论:
可以看到无论我们怎样调换test方法的书写顺序,其测试顺序都是不变的。
目前初步的结论:测试方法执行的顺序与方法名中test后面的字符大小有关,小者优先,例如testA,testB1,testB2三个方法相继执行。
案例 5
Xcode集成了对单元测试的支持,XCode4.x集成的是OCUnit,到了XCode5.x时代就升级为了XCTest,XCode7.x时代XCtest还可以进行UI测试。下面我们简单介绍下XCTest的使用。
在xcode新建项目中,默认会建一个单元测试的target,并建立一个继承于XCTestCase的测试用例类
image.png
本例实现了一个个税计算方法,在测试用例中测试输入后输出是否符合结果。
创建一个名为ASRevenueBL的.h .m文件,如下面所示:
image.png
ASRevenueBL.h
#import@interfaceASRevenueBL:NSObject-(double)calculate:(double)revenue;@end
ASRevenueBL.m
import"ASRevenueBL.h"#definebaseNum 3500.0// 起征点@implementation ASRevenueBL/*
* method:传入收入计算税值
* revenue:收入
*/-(double)calculate:(double)revenue{doubletax=0.0;// 税// 应纳税所得额 = 工资收入金额 - 各项社会保险费 - 起征点(3500元)// 应纳税额 = 应纳税所得额 x 税率 - 速算扣除数doubledbTaxRevenue=revenue-baseNum;if(dbTaxRevenue<=1500){tax=dbTaxRevenue*0.03;}elseif(dbTaxRevenue>1500&&dbTaxRevenue<=4500){tax=dbTaxRevenue*0.1-105;}elseif(dbTaxRevenue>4500&&dbTaxRevenue<=9000){tax=dbTaxRevenue*0.2-555;}elseif(dbTaxRevenue>9000&&dbTaxRevenue<=35000){tax=dbTaxRevenue*0.25-1005;}elseif(dbTaxRevenue>35000&&dbTaxRevenue<=55000){tax=dbTaxRevenue*0.3-2755;}elseif(dbTaxRevenue>55000&&dbTaxRevenue<=80000){tax=dbTaxRevenue*0.35-5505;}elseif(dbTaxRevenue>80000){tax=dbTaxRevenue*0.45-13505;}returntax;}
导入测试方法所在的类的头文件,并创建一个类,在测试方法调用前,初始化类对象,测试完毕后,将对象置nil,其方法测试如下方测试代码:
#import#import"ASRevenueBL.h"@interfaceUnitTestsTwoTests:XCTestCase@property(nonatomic,strong)ASRevenueBL*revenueBL;@end@implementationUnitTestsTwoTests-(void)setUp{[supersetUp];self.revenueBL=[[ASRevenueBL alloc]init];}-(void)tearDown{self.revenueBL=nil;[supertearDown];}-(void)testLevel1{doublerevenue=5000;doubletax=[self.revenueBL calculate:revenue];XCTAssertEqual(tax,45.0,@"测试案例1失败");XCTAssertTrue(tax==45.0);}-(void)testLevel2{XCTestExpectation*exp=[selfexpectationWithDescription:@"超时"];NSOperationQueue*queue=[[NSOperationQueue alloc]init];[queue addOperationWithBlock:^{doublerevenue=1500;doubletax=[self.revenueBL calculate:revenue];sleep(1);NSLog(@"%f",tax);XCTAssertEqual(tax,45,@"用例2测试失败");[exp fulfill];// exp结束}];[selfwaitForExpectationsWithTimeout:3handler:^(NSError*_Nullable error){if(error){NSLog(@"Timeout Error: %@",error);}}];}-(void)testExample{}-(void)testPerformanceExample{[selfmeasureBlock:^{for(inta=0;a<10;a+=a){NSLog(@"%zd",a);}}];}@end
testLevel1通过revenueBL计算出来的tax与预期相同,测试通过;testLevel2通过revenueBL计算出来的tax与预期不同,测试不通过,反映出了程序一些逻辑漏洞;testPerformanceExample中的平均执行时间比基准值低,测试通过。
案例 6 命令行测试
在命令行中也可以启动测试,便于持续集成。
Assuner$ cd Desktop/ Desktop Assuner$ cd ASUnitTestFirstDemo/ ASUnitTestFirstDemo Assuner$ xcodebuild test -project ASUnitTestFirstDemo.xcodeproj -scheme ASUnitTestFirstDemo -destination 'platform=iOS Simulator,OS=11.4,name=iPhone 7' // 可以有多个destination
结果
Test Suite'All tests'started at2017-09-1111:12:16.348Test Suite'ASUnitTestFirstDemoTests.xctest'started at2017-09-1111:12:16.349Test Suite'ASUnitTestFirstDemoTests'started at2017-09-1111:12:16.349Test Case'-[ASUnitTestFirstDemoTests testLevel1]'started.Test Case'-[ASUnitTestFirstDemoTests testLevel1]'passed(0.001seconds).Test Case'-[ASUnitTestFirstDemoTests testLevel2]'started./Users/liyongguang-eleme-iOS-Development/Desktop/ASUnitTestFirstDemo/ASUnitTestFirstDemoTests/ASUnitTestFirstDemoTests.m:46:error:-[ASUnitTestFirstDemoTests testLevel2]:((tax)equal to(45.0))failed:("-60")isnot equal to("45")-用例2测试失败 Test Case'-[ASUnitTestFirstDemoTests testLevel2]'failed(1.007seconds).Test Suite'ASUnitTestFirstDemoTests'failed at2017-09-1111:12:17.358.Executed2tests,with1failure(0unexpected)in1.008(1.009)seconds Test Suite'ASUnitTestFirstDemoTests.xctest'failed at2017-09-1111:12:17.359.Executed2tests,with1failure(0unexpected)in1.008(1.010)seconds Test Suite'All tests'failed at2017-09-1111:12:17.360.Executed2tests,with1failure(0unexpected)in1.008(1.012)seconds Failing tests:-[ASUnitTestFirstDemoTests testLevel2]**TEST FAILED**
如果是workspace
xcodebuild -workspace ASKiwiTest.xcworkspace -scheme ASKiwiTest-Example -destination 'platform=iOS Simulator,OS=11.0,name=iPhone 7' test
每个test方法都会跑一遍,并给出结果描述。
案例 7 代码的执行时间测试-(性能测试)
性能测试主要使用measureBlock方法 ,用于测试一组方法的执行时间,通过设置baseline(基准)和stddev(标准偏差)来判断方法是否能通过性能测试。
假如直接执行方法,因为block中没有内容,所以方法的执行时间为0.0s,如果我们把baseline设成0.05,偏差10%,是可以通过的测试的。但是如果设置如果我们把baseline为1,偏差10%,那测试会失败,因为不满足条件。
如上图所示,这个方法是用来测试block内代码的执行时间的,我们可以通过打印很清楚的看到它其实执行了10次,用处也很宽广,比如想测试身份证的识别时间,请求的时间,转模型的速度等等都可以通过它来测试,这里只是举个简单的例子.
我们可以看下打印发现他确实是执行了十次.
再来看看左边的执行代码相关信息,这里由于打印"1"执行的太快无法看出效果,所以我将测试内容换成了使用for循环打印1-9999,看看他们的执行时间.
可以很清楚的看到,10次的平均时间是1.382秒,第一次时间是1.85秒,并且可以看到第一次执行时间超过了平均时间33%,这里的测试结果都是和机器性能有关系的.
案例 8 登陆模块测试
案例 9 加法测试
-(void)testExample{//设置变量和设置预期值NSUInteger a=10;NSUInteger b=15;NSUInteger expected=24;//执行方法得到实际值NSUInteger actual=[selfadd:a b:b];//断言判定实际值和预期是否符合XCTAssertEqual(expected,actual,@"add方法错误!");}-(NSUInteger)add:(NSUInteger)a b:(NSUInteger)b{returna+b;}
从这也能看出一个测试用例比较规范的写法,1:定义变量和预期,2:执行方法得到实际值,3:断言
案例 10 代码来自于AFNetworking,用于测试backgroundImageForState方法
-(void)testThatBackgroundImageChanges{XCTAssertNil([self.button backgroundImageForState:UIControlStateNormal]);NSPredicate*predicate=[NSPredicate predicateWithBlock:^BOOL(UIButton*_Nonnull button,NSDictionary<NSString*,id>*_Nullable bindings){return[button backgroundImageForState:UIControlStateNormal]!=nil;}];[selfexpectationForPredicate:predicate evaluatedWithObject:self.button handler:nil];[selfwaitForExpectationsWithTimeout:20handler:nil];}
利用谓词计算,button是否正确的获得了backgroundImage,如果正确20秒内正确获得则通过测试,否则失败。
expectationForNotification方法 ,该方法监听一个通知,如果在规定时间内正确收到通知则测试通过。
-(void)testAsynExample1{[selfexpectationForNotification:(@"监听通知的名称xxx")object:nil handler:nil];[[NSNotificationCenter defaultCenter]postNotificationName:@"监听通知的名称xxx"object:nil];//设置延迟多少秒后,如果没有满足测试条件就报错[selfwaitForExpectationsWithTimeout:3handler:nil];}
这个例子也可以用expectationWithDescription实现,只是多些很多代码而已,但是这个可以帮助你更好的理解expectationForNotification方法和expectationWithDescription的区别。同理,expectationForPredicate方法也可以使用expectationWithDescription实现。
functestAsynExample1(){letexpectation=expectationWithDescription("监听通知的名称xxx")letsub=NSNotificationCenter.defaultCenter().addObserverForName("监听通知的名称xxx",object:nil,queue:nil){(not)->Voidinexpectation.fulfill()}NSNotificationCenter.defaultCenter().postNotificationName("监听通知的名称xxx",object:nil)waitForExpectationsWithTimeout(1,handler:nil)NSNotificationCenter.defaultCenter().removeObserver(sub)}
XCTest常见的断言
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没有发生异常时通过测试;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
备注:
1.关于私有方法的测试,只能通过扩展来实现
2.关于case的方法名字,一定要以test开头并注意驼峰命名法,且不能加入参数。
3.单元测试类继承自XCTestCase,他有一些重要的方法,其中最重要的有3个,setUp ,tearDown,measureBlock.
4.md + 5切换到测试选项卡后会看到很多小箭头,点击可以单独或整体测试.
5.cmd + U运行整个单元测试
6.使用pod的项目中,在XC测试框架中测试内容包括第三方包时,需要手动去设置Header Search Paths才能找到头文件 ,还需要设置test target的PODS_ROOT。
7.xcode7要使用真机做跑测试时,证书必须配对,否则会报错exc_breakpoint错误
部分案例Demo:
参考文章:
1-3爱上iOS单元测试系列之爱上她就要先了解她:单元测试入门
1-4Testing with Xcode文档(中文版):从 OCUnit 过渡到 XCTest
1-5iOS关于单元测试的学习笔记还有本人在学的路上遇到的那些 坑
1-6Xcode 5 单元测试(一)使用XCTest进行单元测试
1-7简单iOS单元测试-异步测试(XCTestExpectation)
1-9iOS单元测试
作者:_正阳_
链接:https://www.jianshu.com/p/4001e06b150e
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
网友评论