测试 目录:单元测试+ 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