测试
目录:单元测试+ UI测试+ 自动化测试
单元
测试
目录:
1.逻辑功能测试
2.同,异步功能方法测试 - [分析AFNetworking解释]
3.单元测试之Mock使用简介
4.性能耗时测试
5.单例测试
6.编写测试用例该注意要点
7.封装测试库
1.逻辑
功能测试
1.XCTAssert
断言使用
NSString *name = @"明星"; //判断一个对象不能为空nil
XCTAssertNotNil(name, @"btn should not be nil");
//报错提示语:@"btn should not be nil"
其他方法:
XCTAssertNil 为nil通过
XCTAssertTrue(expression, format...)
当expression求值为TRUE时通过;
XCTAssertEqual(a1, a2, format...)
判断相等
XCTAssertNoThrow(expression, format…)
异常测试
2.方法
无返回值
测试项目中某个方法 - 没有返回值
LoginViewController文件有方法
- (void)loginWithPhone:(NSString *)phone code:(NSString *)code
使用:
我们在方法setup()中声明并创建一个Test对象
然后在方法tearDown()中释放它. (有点像init 和 dealloc )
- (void)setUp {
[super setUp];
//初始化设置
}
- (void)tearDown {
}
测试文件代码:
#import <XCTest/XCTest.h>
#import "LoginViewController.h"
@interface LoginVCtrlTests : XCTestCase
@property(nonatomic,strong)LoginViewController *loginVC;
@end
@implementation LoginVCtrlTests
- (void)setUp {
[super setUp];
self.loginVC = [[LoginViewController alloc]init];
}
- (void)tearDown {
[super tearDown];
self.loginVC = nil;
}
//测试用例
- (void)testExample {
[self.loginVC loginWithPhone:nil code:@"3345"];
[self.loginVC loginWithPhone:null code:nil];
[self.loginVC loginWithPhone:@"" code:null];
}
3.方法
有返回值
功能方法:
- (BOOL)checkPhoneStr:(NSString *)phone {
//判断phone是否合法的代码
BOOL isPhone = .....
return isPhone;
}
测试:
BOOL isPhone = [self.loginVC loginWithPhone:nil code:@"3345"];
XCTAssertTrue(isPhone, @"手机号不合法"); //触发断言
2.多线程
异步方法
(a)异步方法测试流程OC:
- (void)testExample {
//1: 创建XCTestExpectation对象
XCTestExpectation* expect = [self expectationWithDescription:@"请求
超时timeout!"];
//异步方法
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORIT
Y_DEFAULT, 0), ^{
sleep(5); //2: 假设请求需要耗时5秒
NSError *error = [[NSError alloc]init];//3: 假设回调返回一个error
XCTAssertNotNil(error); //4: 对结果进行判断
XCTAssertTrue([error.domain isEqualToString:NSURLErrorDomain]);
dispatch_async(dispatch_get_main_queue(), ^{
//主线程操作....
});
[expect fulfill];//5: 异步结束调用fulfill,告知请求结束(很重要)
});
//超时后执行一些操作(超时方法):
[self waitForExpectationsWithTimeout:15 handler:^(NSError *error) {
//6: 如果15秒内没有收到fulfill方法通知调用次方法
}];
//7: 对象被回收, 不为nil时触发断言
XCTAssertNil(expect, @"expect should be nil");
}
(b)AFNetworking下载图片的单元测试代码(部分) :
// 下载图片测试
- (void)testThatImageDownloaderReturnsNilWithInvalidURL
{
NSMutableURLRequest *mutableURLRequest = [NSMutableURLRequest
requestWithURL:self.pngURL];
[mutableURLRequest setURL:nil];
/** NSURLRequest nor NSMutableURLRequest can be initialized with a
nil URL,
* but NSMutableURLRequest can have its URL set to nil
**/
NSURLRequest *invalidRequest = [mutableURLRequest copy];
XCTestExpectation *expectation = [self
expectationWithDescription:@"Request should fail"];
AFImageDownloadReceipt *downloadReceipt = [self.downloader
downloadImageForURLRequest:invalidRequest
success:nil
failure:^(NSURLRequest * _Nonnull request, NSHTTPURLResponse *
_Nullable response, NSError * _Nonnull error) {
XCTAssertNotNil(error);
XCTAssertTrue([error.domain
isEqualToString:NSURLErrorDomain]);
XCTAssertTrue(error.code ==
NSURLErrorBadURL);
[expectation fulfill]; //异步结束调用
fulfill,告知请求结束, 回到测试方法线程
}];
[self waitForExpectationsWithCommonTimeout]; //超时
XCTAssertNil(downloadReceipt, @"downloadReceipt should be nil");
}
(c)异步请求单元测试Swift代码:
func testAsyncURLConnection(){
let URL = NSURL(string: "http://www.baidu.com")!
let expect = expectation(description: "GET \(URL)")
let session = URLSession.shared
let task = session.dataTask(with: URL as URL, completionHandler:
{(data, response, error) in
XCTAssertNotNil(data, "返回数据不应该为空")
XCTAssertNil(error, "error应该为nil")
expect.fulfill() //请求结束通知测试
if response != nil {
let httpResponse: HTTPURLResponse = response as!
HTTPURLResponse
XCTAssertEqual(httpResponse.statusCode, 200, "请求失败!")
DispatchQueue.main.async {
//主线程中干事情
}
} else {
XCTFail("请求失败!")
}
})
task.resume()
//请求超时
waitForExpectations(timeout:
(task.originalRequest?.timeoutInterval)!, handler: {error in
task.cancel()
})
}
3. 单元测试
之Mock使用
Mock使用文档:
http://ocmock.org/introduction/
Mock干啥的 ? :
在测试过程中,对于一些不容易构造或不容易获取的对象,此时你可以创建一个
虚拟的对象(mock object)来完成测试, Mock却很方便,它直接返回你需要的数
据,不用初始化对象,避免复杂的数据获取过程
- (void)testDisplaysTweetsRetrievedFromConnection
{
Controller *controller = [[[Controller alloc] init] autorelease];
//声明id类型对象(不需要TwitterConnection类直接初始化对象)
id mockConnection = OCMClassMock([TwitterConnection class]);
controller.connection = mockConnection;
Tweet *testTweet = /* create a tweet somehow */;
NSArray *tweetArray = [NSArray arrayWithObject:testTweet];
//模拟返回数据
OCMStub([mockConnection fetchTweets]).andReturn(tweetArray);
[controller updateTweetView];
}
比如创建tableview测试:
id mockTableView = [OCMockObject mockForClass:[UITableView
class]];
UITableViewCell *cell = [[UITableViewCell alloc] init];
[[[mockTableView expect] andReturn:cell]
dequeueReusableCellWithIdentifier:@"MockTableViewCell" forIndexPath:
[NSIndexPath indexPathForRow:0 inSection:0]];
4.性能
耗时测试
当项目创建完测试文件时,OC就会自动创建下面方法:
- (void)testPerformanceExample {
// This is an example of a performance test case.
[self measureBlock:^{
// 处理耗时操作.
}];
}
[self measureBlock:^{
// 耗时操作
NSMutableDictionary *dic = @{}.mutableCopy;
for (NSInteger i = 0; i < 10000; i++) {
NSString *obj = [NSString stringWithFormat:@"%ld",(long)i];
[dic setObject:obj forKey:obj];;
}
}];
也可以通过NSTimeInterval start(end) = CACurrentMediaTime(); 计算差值
5. 单例测试
单例: 不管什么方法初始化都是返回同一个对象, 测试依据也是如此 :
- (void)testFilesManagerSingle
{
//思路 :多种方法创建单例类对象,放入一个数组中, 最后判断他们是否相同
NSMutableArray *managerArray = [NSMutableArray array];//栈
//alloc 初始化
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORIT
Y_DEFAULT, 0), ^{
FilesManager *tempManager = [[FilesManager alloc] init];
[managerArray addObject:tempManager];
});
//shareManager 初始化
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORIT
Y_DEFAULT, 0), ^{
FilesManager *tempManager = [FilesManager shareManager];
[managerArray addObject:tempManager];
});
FilesManager *managerObj = [FilesManager shareManager];
//最后判断他们是否相同
[managerArray enumerateObjectsUsingBlock:^(FilesManager *obj,
NSUInteger idx, BOOL * _Nonnull stop) {
XCTAssertEqual(managerObj, obj, @"FilesManager is not single");
}];
}
6.封装测试库
跟项目文件类似, 不同类的测试用例创建不同的测试文件, 命名并以Tests结尾
(1) 当你的测试内容越来越多时,测试代码就像工程一样,甚至更复杂, 同样单元测
试也需要封装,继承,设计等等.
(2) 异步和性能测试往往比较耗时,所以要注意和逻辑测试等分开测试
(3) 测试框架有好几个,对于中小型项目个人觉得考虑兼容性直接使用XCTest
(4) 公用方法等尽量抽离或者写一个宏,比如本节中单例,或者[self
waitForExpectationsWithCommonTimeout]; 方法写一个TimeoutTest宏等
等.
框架:
OCUnit 是 OC 官方测试框架, 现在被 XCTest 所取代
XCTest (系统框架)是与 Foundation 框架平行的测试框架。
GHUnit 是第三方的测试框架
https://github.com/gh-unit/gh-unit
OCMock都是第三方的测试框架
https://github.com/erikdoe/ocmock
7.覆盖率
测试
Code Coverage
设置 : Xcode 导航点击项目 -> 编辑scheme -> Test -> 选中coverage data,
然后关闭,现在已经设置好了
运行 : Xcode 导航 product -> Test 运行测试用例
最后,测试完成
跳转未被测试的文件代码:将光标放置在文件名称上,出现一个箭头,点击箭头即可
UI测试
系统录制自动化测试 ,基于XCTest
1. 创建项目时,选中Include UI Tests
2. 若是老项目没有, file-> new-> target 添加
3. 如何添加(录制)UI测试代码 ?
将光标定位在testMainViewClickBtn方法中,点击底部红点[有时候红点不能点击,
将xcode关掉再打开一般就好了]开始录制UTTest代码:
录制过程操作需要的相关步骤, 再点击红色点按钮后退出录制
系统默认录制的代码会有错误的时候,需要优化一下
func testMainViewClickBtn() {
let app = XCUIApplication()
app.buttons["pushToNextPage"].tap()
app.navigationBars["subView"].buttons["mainView"].tap()
app.buttons["doSomething"].tap()
app.otherElements.containing(.navigationBar,
identifier:"mainView").children(matching:
.other).element.children(matching: .other).element.children(matching:
.other).element.tap()
}
自动化测试
第三方自动化框架 :
KIF
Appium
Quick
Calabash
Frank
集成工具 :
Jenkins +fastlane +pgyer +webHook
KIF
1. KIF使用标准的XCTest测试目标来构建和执行测试(第三方UI测试框架)
2.KIF使用未公开的Apple API
3.所有的KIF测试都是用Objective-C编写的
pod 导入 (注意是创建的UnitTest)
target 'MyAppTests' do
pod 'KIF'
end
使用前提是 控件的要设置accessibilityLabel 属性
所有的测试方法要以test头
func validateLogin(){
let invalicaodeInput = tester().usingLabel("login_input_invlicode")
invalicaodeInput?.enterText("123456")
let valicodeBtn = tester().usingLabel("获取验证码")
valicodeBtn?.tap()
let loginBtn = tester().usingLabel("登录")
loginBtn?.tap()
tester().wait(forTimeInterval: 2)
}
func agreeValidateLogin(){
let login_agree = tester().usingLabel("login_agree")
login_agree?.tap()
let loginBtn = tester().usingLabel("登录")
loginBtn?.tap()
let firstPage = tester().usingLabel("首页").waitForView()
if (firstPage != nil) {
loginOut()
} else {
XCTAssert(false, "未找到页面")
}
}
func loginOut(){
let meBtn = tester().usingLabel("我")
meBtn?.tap()
let setUpBtn = tester().usingLabel("设置")
setUpBtn?.tap()
let outBtn = tester().usingLabel("退出登录")
outBtn?.tap()
tester().wait(forTimeInterval: 5)
}
jenkins集KIF成步骤
1. 项目上传GitLab
2.启动jenkins,新建item,构建一个自由风格的项目
3.源码管理
(设置git账号,密码) , 设置要拉取的分支版本
4.构建触发器
自定义触发脚本运行时机。比如设置构建触发器*/2 * * * *,每2分钟检查一次源
码变化
5.构建(脚本)
添加Execute Shell 脚本
mac终端安装ocunit2junit 以及slather
sudo gem install ocunit2junit
sudo gem install slather
6.构建后操作
读取显示junit和覆盖率html报告 :
安装两个jenkins插件,jenkins->系统管理-> 管理插件,找到JUnit Plugin和
HTML Publisher plugin,安装重启jenkins
添加选项Publish Junit test result report,配置xml
添加选项Publish HTML reports, 覆盖率的图表
构建完成查看测试结果报告和覆盖率 :
7.选择部分测试用例运行:
iOS持续集成
1. 自动化的构建(包括编译,发布,自动化测试)
配置Jenkins +fastlane +pgyer +webHook
流程:
上传代码到GitLab
webHook(钩子)通知Jenkins
Jenkins收到消息自动触发构建拉取上传的最新代码,执行Shell脚本完成自动化
测试,自动化代码检查,打包上传蒲公英等第三方App托管平台
第三方App托管平台(fir 或 蒲公英)通过短信和邮件通知测试人员
2. 如果是单人开发在本机构建,可只安装使用fastlane
3. Mac 环境配置 :
安装ruby环境 > 2.0
安装brew
$ ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/
install/master/install)"
Java环境
Jenkins依赖于Java环境 :
$ brew cask install java
单人构建fastlane :
Fastlane 是一个 ruby 脚本集合成的套件, 包括了向 App Store 提交新应用或
更新已有应用所需要的常用任务 :
功能 :
1.gym 编译打包生成 ipa 文件
2.deliver 用于上传应用的二进制代码,应用截屏和元数据到 App Store
3.sigh 可以生成并下载开发者的 App Store 配置文件
4.snapshot 可以自动化iOS应用在每个设备上的本地化截屏过程
安装fastlane :
1. 终端执行 : xcode-select --install
2. 点击安装
$ brew cask install fastlane (输入密码等待安装成功)
安装成功后提示: fastlane was successfully installed!
$ export PATH="$HOME/.fastlane/bin:$PATH" (安装成功后执行)
$ fastlane env (查看fastlane当前环境,会提示你是否复制到剪切板,输入n即
可)
$ fastlane -version (输出版本信息即为成功)
3.安装蒲公英插件
$ fastlane add_plugin pgyer
4.在.xcodeproj项目目录下,初始化fastlane:
$ fastlane init
终端会提示要你填写你的开发者账号与密码,然后fastlane会自动检测当前目录
下项目的App Name和App Identifier、Project。然后自行确认并按流程执行
5.项目使用了Cocopods配置:
在Gemfile文件中加入代码:
gem "cocoapods"
多人Jenkins集成 :
安装Jenkins :
tomcat+war部署Jenkins
安装好tomcat环境,下载war文件包,将war文件移动到Tomcat文件夹的webapps
目录下
打开链接 localhost:8080/jenkins/ 启用Jenkins
其它方式(优先tomcat) : brew安装
pkg安装
配置Jenkins :
1. 网上参考流程去配置
2.shell脚本 :
3.安装插件(安装完成重启)
Gitlab Hook Plugin
GitLab Plugin
Build Authorization Token Root Plugin
4.配置 构建触发器:
勾选触发远程构建 :
填写身份令牌 : 在终端输入如下命令,获取Token令牌
$ openssl rand -hex 12
勾选Build when a change is pushed to GitLab. GitLab CI Service URL选项
5. 根据身份验证令牌下的提示,拼接webHook URL
提示 : JENKINS_URL/job/tyfocgApp(iOS)/build?token=TOKEN_NAME 或者
/buildWithParameters?token=TOKEN_NAME
拼接 : http://192.168.xx.xx:8080/buildByToken/build?
job=tyfocgApp(iOS)&token=26acd09446289127aaa7f8d0
6. 进入GitLab网页, 设置,
选择Web Hook 填入上方的地址,
点击AddWebhook即可
webhooks ?
钩子功能(callback),是帮助用户push了代码后,自动回调一个您设定的http
地址。 这是一个通用的解决方案,用户可以自己根据不同的需求,来编写自己的
脚本程序(比如发邮件,自动部署等),例如你提交代码到仓库,钉钉上会有消息
通知,也是通过钩子实现的。
点击下方的Test Hook按钮测试此链接是否ok
重启一下GitLab服务器
push代码到develoer分支,你会发现Jenkins自动执行构建任务,checkout代
码, 触发脚本打包上传蒲公英
fastfile脚本
fastlane使用:
配置完下面脚本后, cd到项目.xcworkspace目录, 执行下面命令 :
$ fastlane automaticPackagingUpload
打开项目fastlane目录下的文件夹,将下列脚本代码替换到Fastfile文件中 :
#使用方法 cd到项目.xcworkspace目录 终端输入 fastlane
automaticPackagingUpload
# 定义fastlane版本号
fastlane_version “2.55.0”
# 定义打包平台
default_platform :ios
#指定项目的scheme名称
scheme = “ scheme”
#蒲公英api_key和user_key
api_key = “api_key”
user_key = “user_key”
def updateProjectBuildNumber
currentTime = Time.new.strftime("%Y%m%d")
build = get_build_number()
if build.include?"#{currentTime}."
# => 为当天版本 计算迭代版本号
lastStr = build[build.length-2..build.length-1]
lastNum = lastStr.to_i
lastNum = lastNum + 1
lastStr = lastNum.to_s
if lastNum < 10
lastStr = lastStr.insert(0,"0")
end
build = "#{currentTime}.#{lastStr}"
else
# => 非当天版本 build 号重置
build = "#{currentTime}.01"
end
puts("*************| 更新build #{build} |*************")
# => 更改项目 build 号
increment_build_number(
build_number: "#{build}"
)
end
# 任务脚本
platform :ios do
lane :automaticPackagingUpload do|options|
branch = options[:branch]
puts “*************| 开始打包.ipa文件 |*************”
updateProjectBuildNumber #更改项目build号
# 开始打包
gym(
#输出的ipa名称
output_name:”#{scheme}_#{get_build_number()}”,
#指定项目的scheme
scheme:"#{scheme}",
# 是否清空以前的编译信息 true:是
clean:true,
# 指定打包方式,Release 或者 Debug
configuration:"Release",
# 指定打包所使用的输出方式,目前支持app-store, package, ad-hoc,
enterprise, development
export_method:"ad-hoc",
# 指定输出文件夹
output_directory:"~/Desktop/fastlaneBuild",
)
puts “*************| 开始上传蒲公英 |*************”
# 开始上传蒲公英
pgyer(api_key: “#{api_key}”, user_key: “#{user_key}”)
puts “*************| 上传蒲公英成功!|*************”
end
end