单元测试(Unit Testing),又称为模块测试,是指对软件中的最小可测试单元进行检查和验证,通过开发者编写代码去验证被测代码是否正确的一种手段,例如编写一个测试函数去测试某一功能函数是否能正确执行达到预期效果。在实际项目开发中使用单元测试可以提高软件的质量,也可以尽量早的发现代码中存在的问题加以修正。
执行单元测试,是为了证明某段代码的行为确实和开发者所期望的一致。因此,我们所要测试的是规模很小的、非常独立的功能片段。通过对所有单独部分的行为建立起信心。然后,才能开始测试整个系统。
持续集成(Continuous Integration),简称CI,是软件开发周期的一种实践,把代码仓库(Gitlab或者Github)、构建工具(如Jenkins)和测试工具(SonarQube)集成在一起,频繁的将代码合并到主干然后自动进行构建和测试。简单来说持续集成就是一个监控版本控制系统中代码变化的工具,当发生变化是可以自动编译和测试以及执行后续自定义动作。

点击下载Demo:ZJHUnitTestDemo
在创建项目时勾选 Include Tests选项,如下图所示:

创建项目成功后,项目目录下即可看到对应的单元测试文件夹。先忽略ZJHUnitTestDemoUITests,它属于UI测试,其他文章会有更多介绍,本文主要讲ZJHUnitTestDemoTests文件

如果之前的项目还没有添加单元测试target,也可以按照下图方式进行新建:

在新建的测试文件代码如下所示,系统自动生成了几个方法:
#import <XCTest/XCTest.h>
// 所有的测试类需要继承 XCTestCase
@interface ZJHUnitTestDemoTests : XCTestCase
@end
@implementation ZJHUnitTestDemoTests
/// 在每一个测试方法调用前,都会被调用;用来初始化 test 用例的一些初始值
- (void)setUp {
// Put setup code here. This method is called before the invocation of each test method in the class.
// 在这里设置代码。在调用类中的每个测试方法之前调用此方法。
}
/// 在每一个测试方法调用后,都会被调用;用来重置 test 方法的数值
- (void)tearDown {
// Put teardown code here. This method is called after the invocation of each test method in the class.
// 在这里输入删除代码。在调用类中的每个测试方法之后调用此方法。
}
/// 测试方法命名以 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.
// 这是一个功能测试用例。
// 使用XCTAssert和相关函数来验证您的测试产生正确的结果。
}
/// 性能测试
- (void)testPerformanceExample {
// This is an example of a performance test case.
// 这是一个性能测试用例。
[self measureBlock:^{
// Put the code you want to measure the time of here.
// 把你想要测量时间的代码放在这里。
}];
}
@end
setUp方法:setUp方法会在XCTestCase的测试方法每次调用之前调用,所以可以把一些测试代码需要用的初始化代码和全局变量写在这个方法里;
tearDown:在每个单元测试方法执行完毕后,XCTest会执行tearDown方法,所以可以把需要测试完成后销毁的内容写在这个里,以便保证下面的测试不受本次测试影响
测试用例:所有测试的方法都需要以test为前缀进行命名,比如- (void)testExample,- (void)testPerformanceExample
我们在项目里面创建一个ZJHMathTool类:
@interface ZJHMathTool : NSObject
- (int)sumA:(int)a andB:(int)b;
- (int)subA:(int)a andB:(int)b;
- (int)multiplyA:(int)a andB:(int)b;
- (int)divideA:(int)a andB:(int)b;
@end
@implementation ZJHMathTool
- (int)sumA:(int)a andB:(int)b {
return a + b;
}
- (int)subA:(int)a andB:(int)b {
return a - b;
}
- (int)multiplyA:(int)a andB:(int)b {
return a * b;
}
- (int)divideA:(int)a andB:(int)b {
return a / b;
}
@end
同时在新建一个对应的 ZJHMathToolTests测试类:
#import <XCTest/XCTest.h>
@interface ZJHMathToolTests : XCTestCase
@end
@implementation ZJHMathToolTests
- (void)setUp {
}
- (void)tearDown {
}
- (void)testExample {
}
- (void)testPerformanceExample {
[self measureBlock:^{
}];
}
@end
接下来我们开始编写用例,来测试ZJHMathTool中的方法,如下所示
@interface ZJHMathToolTests : XCTestCase
@property (nonatomic, strong) ZJHMathTool *mathTool;
@end
@implementation ZJHMathToolTests
// 新建ZJHMathTool对象
- (void)setUp {
self.mathTool = [ZJHMathTool new];
}
// 销毁ZJHMathTool对象
- (void)tearDown {
self.mathTool = nil;
}
// 测试加法
- (void)testMathAdd {
int result = [self.mathTool sumA:2 andB:3];
XCTAssert(result == 5, @"加法计算出错");
}
// 测试减法
- (void)testMathSub {
int result = [self.mathTool subA:5 andB:2];
XCTAssert(result == 3, @"减法计算出错");
}
@end
运行测试用例 :
代码编辑器边栏菱形按钮,测试单个用例
Test 导航栏,测试单个用例
快捷键 command + U测试全部用例
使用命令行工具 xcodebuild 可以测试单个用例,也可以测试全部用例

性能测试通过度量代码块执行所消耗的时间长短,来衡量是否通过测试。
新建 ZJHPerson 类,然后添加一个循环打印方法。
@interface ZJHPerson : NSObject
- (void)sayHello;
@end
@implementation ZJHPerson
- (void)sayHello {
for (int i = 0; i < 1000; i++) {
NSLog(@"hello");
}
}
@end
然后再新建 ZJHPerson 对象测试类 ZJHPersonTests。
#import <XCTest/XCTest.h>
#import "ZJHPerson.h"
@interface ZJHPersonTests : XCTestCase
@property (nonatomic, strong) ZJHPerson *person;
@end
@implementation ZJHPersonTests
- (void)setUp {
self.person = [ZJHPerson new];
}
- (void)tearDown {
self.person = nil;
}
- (void)testPerformanceExample {
[self measureBlock:^{
[self.person sayHello];
}];
}
@end
有两个API可以使用
- measureBlock主要是通过block内部代码块的执行时间来测试性能,通过设置baseline(基准)和stddev(标准偏差)来判断方法是否能通过性能测试。
- (void)testPerformanceOfMyFunction {
[self measureBlock:^{
//做你想测量的东西。
MyFunction();
}];
}
- measureMetrics:automaticallyStartMeasuring:forBlock测量代码块的性能,可以选择推迟测量的起点。
- startMeasuring在代码块中开始性能度量。
- stopMeasuring结束代码块内的性能度量。
defaultPerformanceMetrics标识在调用measureBlock:时度量的性能指标。
XCTPerformanceMetricXCTest可以测量的性能指标。
- (void)testMyFunction2_WallClockTime {
[self measureMetrics:[self class].defaultPerformanceMetrics automaticallyStartMeasuring:NO forBlock:^{
// 做设置工作,需要为每个迭代,但你不希望在调用- startmeasurement之前进行测量
SetupSomething();
[self startMeasuring];
// 做你想测量的东西。
MyFunction();
[self stopMeasuring];
//执行每次迭代都需要执行的分解工作,但你不想在调用- stopmeasurement后进行度量
TeardownSomething();
}];
}
所有的性能测试需要设置一个Baseline来验证是否通过测试,没有设置的会提示No baseline average for Time。点击左边灰色菱形图标可查看性能测试结果。

在性能测试结果图里可以看到平均时间(总时长/10),还有10个柱状图,这个意思是在这个测试方法运行总时长被分为10份,蓝色柱子表示每份的耗时,中间的横线表示平均时间,点击数字可查看每份中的平均时长。
点击Edit可以进行编辑,我设置的基准时间是0.15s,最大容错率是10%,运行结果是0.147s,好于基本先2%,所有可以通过。设置时基准时间为0.05s时就会出错。

什么时候需要使用异步测试:
异步测试分为3个部分: 新建期望 、等待期望被履行 和 履行期望 。
XCTestExpectation:测试期望,可以由测试类持有,也可以自己持有,自己持有测试期望时灵活性更好一些,你可以选择等待哪些期望。
waitForExpectations:timeout: :等待异步的期望代码执行,根据初始化方式不同,等待的方法不同。
fulfill :履行期望,并且适当加入XCTAssertTrue等断言,来验证测试结果。
/// 异步测试XCTestExpectation:测试类持有期望
- (void)testAsyncMethod1 {
// 新建期望:测试类持有的初始化方法
XCTestExpectation *expect1 = [self expectationWithDescription:@"asyncTest1"];
// 履行期望:执行异步操作
[ZJHNetworkTool requestUrl:@"getTestData" param:@{} completion:^(NSDictionary * _Nonnull respondDic) {
// 异步结束,标注期望达成
[expect1 fulfill];
}];
// 等待期望被履行:测试类持有时的等待方法
[self waitForExpectationsWithTimeout:3.0 handler:^(NSError * _Nullable error) {
NSLog(@"***ZJH error : %@", error);
}];
}
/// 异步测试XCTestExpectation:自己类持有期望
- (void)testAsyncMethod2 {
// 新建期望:自己持有的初始化方法
XCTestExpectation *expect2 = [[XCTestExpectation alloc] initWithDescription:@"asyncTest2"];
// 履行期望:执行异步操作
[ZJHNetworkTool requestUrl:@"getTestData" param:@{} completion:^(NSDictionary * _Nonnull respondDic) {
XCTAssertTrue([respondDic[@"code"] isEqualToString:@"200"]);
// 异步结束,标注期望达成
[expect2 fulfill];
}];
// 等待期望被履行:自己持有时的等待方法
[self waitForExpectations:@[expect2] timeout:3];
}
XCTWaiter是 2017 年新增的异步测试方案,可以通过代理方式来处理异常情况。
XCTWaiterDelegate:如果委托是XCTestCase实例,下方代理被调用时会报告为测试失败。
/// 异步测试XCTWaiter
- (void)testAsyncMethod3 {
// 新建期望
XCTWaiter *waiter = [[XCTWaiter alloc] initWithDelegate:self];
XCTestExpectation *expect3 = [[XCTestExpectation alloc] initWithDescription:@"asyncTest3"];
// 履行期望:执行异步操作
[ZJHNetworkTool requestUrl:@"getTestData" param:@{} completion:^(NSDictionary * respondDic) {
XCTAssertTrue([respondDic[@"code"] isEqualToString:@"200"]);
// 异步结束,标注期望达成
[expect3 fulfill];
}];
// 等待期望被履行
XCTWaiterResult result = [waiter waitForExpectations:@[expect3]
timeout:3
enforceOrder:NO];
XCTAssert(result == XCTWaiterResultCompleted, @"failure: %ld", result);
}
// 如果有期望超时,则调用。
- (void)waiter:(XCTWaiter *)waiter didTimeoutWithUnfulfilledExpectations:(NSArray<XCTestExpectation *> *)unfulfilledExpectations {
NSLog(@"***ZJH 如果有期望超时,则调用。");
}
// 当履行的期望被强制要求按顺序履行,但期望以错误的顺序被履行,则调用。
- (void)waiter:(XCTWaiter *)waiter fulfillmentDidViolateOrderingConstraintsForExpectation:(XCTestExpectation *)expectation requiredExpectation:(XCTestExpectation *)requiredExpectation {
NSLog(@"***ZJH 当履行的期望被强制要求按顺序履行,但期望以错误的顺序被履行,则调用。");
}
// 当某个期望被标记为被倒置,则调用。
- (void)waiter:(XCTWaiter *)waiter didFulfillInvertedExpectation:(XCTestExpectation *)expectation {
NSLog(@"***ZJH 当某个期望被标记为被倒置,则调用。");
}
// 当 waiter 在 fullfill 和超时之前被打断,则调用。
- (void)nestedWaiter:(XCTWaiter *)waiter wasInterruptedByTimedOutWaiter:(XCTWaiter *)outerWaiter {
NSLog(@"***ZJH 当 waiter 在 fullfill 和超时之前被打断,则调用。");
}
在写测试用例的时候,我们可以使用断言,下面是记录一下:
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。例如
Command+shit+, 调出工程配置 Test->Options->Code Coverage勾选上

运行测试后,command+9或 者点击工程左上角最后一个图标查看覆盖报告

双击方法名或者点击方法名右侧的箭头可以跳转到该方法中。右侧有数字,0表示没有覆盖掉,1表示覆盖了一次,调用了几次数字就会变成几。

在 Xcode 10 中新增功能,在 Edit Scheme -> Test -> Info -> Tests 中可以通过取消勾选,来选择跳过部分测试用例。在 target 的 Options 选项中,Automatically includes new tests,选项是默认勾选的,新建的测试文件会自动添加进去。

默认情况下,测试用例执行的顺序是按字母顺序来执行的,按固定顺序执行可能会使一些隐式的依赖关系无法被发现。现在有了随机的执行顺序,就可以挖掘出那些隐式的依赖关系。可以在 Edit Scheme -> Test -> Info -> Tests -> Options 中开启该功能。

并行测试可以同时进行多个测试,从而节省大量时间。在测试时会启动多个模拟器,模拟器之间的数据都是隔离的,可以在 Edit Scheme -> Test -> Info -> Tests -> Options 中开启该功能。如上图
对于并行测试的一些建议:
参考链接:
iOS单元测试:https://www.jianshu.com/p/fcd82723f134
iOS开发之单元测试:https://www.jianshu.com/p/6f4f9fe5f1e1
iOS 单元测试和 UI 测试快速入门:https://juejin.cn/post/6844903744170098695
iOS单元测试:https://juejin.cn/post/6844904138388537352
我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div
我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看rubyzip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d
类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc
很好奇,就使用rubyonrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提
假设我做了一个模块如下:m=Module.newdoclassCendend三个问题:除了对m的引用之外,还有什么方法可以访问C和m中的其他内容?我可以在创建匿名模块后为其命名吗(就像我输入“module...”一样)?如何在使用完匿名模块后将其删除,使其定义的常量不再存在? 最佳答案 三个答案:是的,使用ObjectSpace.此代码使c引用你的类(class)C不引用m:c=nilObjectSpace.each_object{|obj|c=objif(Class===objandobj.name=~/::C$/)}当然这取决于
我正在尝试使用ruby和Savon来使用网络服务。测试服务为http://www.webservicex.net/WS/WSDetails.aspx?WSID=9&CATID=2require'rubygems'require'savon'client=Savon::Client.new"http://www.webservicex.net/stockquote.asmx?WSDL"client.get_quotedo|soap|soap.body={:symbol=>"AAPL"}end返回SOAP异常。检查soap信封,在我看来soap请求没有正确的命名空间。任何人都可以建议我
关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。
我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t
我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h
我想为Heroku构建一个Rails3应用程序。他们使用Postgres作为他们的数据库,所以我通过MacPorts安装了postgres9.0。现在我需要一个postgresgem并且共识是出于性能原因你想要pggem。但是我对我得到的错误感到非常困惑当我尝试在rvm下通过geminstall安装pg时。我已经非常明确地指定了所有postgres目录的位置可以找到但仍然无法完成安装:$envARCHFLAGS='-archx86_64'geminstallpg--\--with-pg-config=/opt/local/var/db/postgresql90/defaultdb/po