菜单

单元测试+UI测试+自动化测试 -OC

2019年12月6日 - iOS开发

•    iOS单元测试+UI测试+自动化测试
•    参考:
•     https://www.jianshu.com/p/c060ed662740
•    单元测试
•    目录:

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方法:

- (void)setUp {
[super setUp];
//初始化设置
}
- (void)tearDown {
}

使用:
我们在方法setup()中声明并创建一个Test对象
然后在方法tearDown()中释放它. (有点像init 和 dealloc )

•    测试文件代码:

#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_PRIORITY_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_PRIORITY_DEFAULT, 0), ^{
FilesManager *tempManager = [[FilesManager alloc] init];
[managerArray addObject:tempManager];
});
//shareManager 初始化
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_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
•    可以参考网上博客学习,比如:
•    iOS KIF自动化测试
•    https://www.jianshu.com/p/dea69545bf4e
•    iOS持续集成
•    https://www.jianshu.com/p/3511625b2ffc
•    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

标签:

发表评论

电子邮件地址不会被公开。