美文网首页
iOS自动化测试(1)

iOS自动化测试(1)

作者: 在路上浅谈管理 | 来源:发表于2019-07-05 15:58 被阅读0次

    首先:先说明一下,我自己自动化测试用的是Xcode自带的测试工具XCTest框架来进行产品的UI以及单元测试测试的。对于大部分公司而言,Xcode自带的自动化测试工具,使用起来也很成熟,完全卓卓有余了。当然你自己也可以研究一些其他的第三方的自动化测试框架:比如:

    1.Appium

    它提供了什么:开源支持的测试
    适用于iPhone,iPad和Android
    它要多少钱?它是完全免费的。
    它最大的优势是什么?
    有一个巨大的开源社区:Appium是迄今为止领先的跨平台(移动)本机测试自动化的开源测试框架。
    您无需重新编译应用程序或引入任何其他软件。
    它适用于各种测试平台和语言。
    开发人员已经开始依赖Appium作为他们最喜欢的测试工具之一。它在本机和混合移动应用程序开发人员中很受欢迎,这主要归功于它的灵活性和广泛的开源社区。
    由Sauce Labs构建和支持,Appium是获得大量建议和支持的门户:如果您在测试期间遇到任何问题,其庞大的开源系列将为您指明方向。
    更重要的是,它是用户友好的iOS测试工具之一。您无需在应用中安装SDK,并且由于它使用标准API,因此无需重新编译应用程序。

    2. Calabash

    它提供了什么:能够用简单的英语写作,能够测试本地和混合应用程序
    Xamarin和Cucumber功能
    它要多少钱?
    再说一遍,绝对没有。
    它最大的优势是什么?
    几乎任何人都可以使用它
    您可以访问大量云设备
    它非常通用 - 适用于物理设备和模拟器
    与Appium一样,Calabash是一个开源移动自动化测试项目。而且,和Appium一样,它的简洁性也是如此。虽然它不像Appium那样包罗万象,但确实有一些特定的优点。
    最大的优势是Calabash利用Cucumber功能,这意味着测试可以用简单的英语而不是迷宫式代码链编写。因此,测试任务可以委派给任何团队成员,程序员或其他人。

    3.KIF - 保持功能

    它提供了什么:特定于UI的测试工具,与XCUITest密切相关
    它要多少钱?
    绝对零。
    它最大的优势是什么?
    与Apple同步
    现实的测试条件
    KIF与XCUITest密切相关,专门用于测试用户界面。同样,它与Xcode的紧密关系具有明显的优势:它可以直接集成到Xcode项目中,免除了使用额外的Web服务器或使用任何其他软件包耗尽内存的麻烦。
    设置非常简单。如果您的项目已经设置了XCTests,您只需要将KIF pod添加到您的项目中。而且测试甚至比XC工具更快。

    4. TestComplete

    它提供了什么:本机iOS应用程序的UI测试
    几个下一代功能,包括对象识别和记录和回放
    它要多少钱?
    高达$ 7,000。
    它最大的优势是什么?
    使用AI
    简单,支持跨平台测试
    作为测试领域的一个相对新人,TestComplete正在凭借其尖端技术掀起波澜。如果您愿意为自动UI测试工具付出一些代价,这可能是一个非常好的选择。
    对于可以达到7,000美元的套餐,TestComplete肯定不便宜。但它支持跨平台测试,其中一些功能特别具有破坏性。该工具使用AI提供对象识别技术,让您对应用程序的UI有更直观的印象。还有许多其他很酷的功能,例如无脚本关键字测试和记录和回放功能。

    以及还有其他sonar什么的,都可以自己去研究,这里就不多说了。文章后面会继续讲解一些

    大多数的iOS App(没有持续集成)迭代流程是这样的:


    image.png

    也就是说,测试是发布之前的最后一道关卡。如果bug不能在测试中发现,那么bug就会抵达用户,所以测试的完整性和可靠性十分重要。

    目前,大多数App还停留在人工测试阶段,人工测试投入的成本最低,能够保证核心功能的使用,而且测试人员不需要会写代码。

    但是,在很多测试场景下,人工测试的效率太低,容易出错。举两个常见的例子:

    一个App的核心功能,在每一次发布版本前的测试必定会跑一遍所有的测试用例,不管对应的业务在当前版本有没有变化(天知道开发在做业务A的时候,对业务B有没有影响),如果这次测出新的bug,测试人员在下一次发版测试中,又不得不做这些重复的工作。

    开发在写API请求相关代码的时候没有做数据容错,测试在人工测试的时候都是正常的数据,所以测试通过。上线了之后,后台配置数据的时候出了点小问题,导致大面积崩溃。

    自动化测试:
    自动化测试就是写一些测试代码,用代码代替人工去完成模块和业务的测试。
    其实不管是开发还是测试,如果你在不断的做重复性工作的时候,就应该问自己一个问题:是不是有更高效的办法?

    自动化测试有很多优点:
    测试速度快,避免重复性的工作
    避免regression,让开发更有信心去修改和重构代码(个人认为最大的优点)
    具有一致性。
    有了自动化测试,持续集成(CI)会变得更可靠。
    迫使开发人员写出更高质量的代码。(自动化测试不通过,代码不允许合并)
    当然,自动化测试也有一些缺点。
    开发和维护成本高。
    不能完全替代人工测试。
    无法完全保证测试的准确性 - 让代码去判断一段逻辑是否正确很容易,但是,让代码判断一个控件显示是否正确却没那么容易。
    所以,在做自动化测试之前,首先要问自己几个问题?
    这个测试业务的变动是否频繁?
    这个测试业务是否属于核心功能?
    编写测试代码的成本有多少?
    自动化测试能保证测试结果的准确么?
    通常,我们会选择那些业务稳定,需要频繁测试的部分来编写自动化测试脚本,其余的采用人工测试,人工测试仍然是iOS App开发中不可缺少的一部分。

    测试种类
    从是否接触源代码的角度来分类:测试分为黑盒和白盒(灰盒就是黑盒白盒结合,这里不做讨论)。

    白盒测试的时候,测试人员是可以直接接触待测试App的源代码的。白盒测试更多的是单元测试,测试人员针对各个单元进行各种可能的输入分析,然后测试其输出。白盒测试的测试代码通常由iOS开发编写。

    黑盒测试。黑盒测试的时候,测试人员不需要接触源代码。是从App层面对其行为以及UI的正确性进行验证,黑盒测试由iOS测试完成。

    从业务的层次上来说,测试金字塔如图:

    image.png

    而iOS测试通常只有以下两个层次:
    Unit,单元测试,保证每一个类能够正常工作
    UI,UI测试,也叫做集成测试,从业务层的角度保证各个业务可以正常工作。

    框架选择

    啰里八嗦讲的这么多,自动化测试的效率怎么样,关键还是在测试框架上。那么,如何选择测试框架呢?框架可以分为两大类:XCode内置的和三方库。

    选择框架的时候有几个方面要考虑

    测试代码编写的成本

    是否可调式

    框架的稳定性

    测试报告(截图,代码覆盖率,…)

    WebView的支持(很多App都用到了H5)

    自定义控件的测试

    是否需要源代码

    能否需要连着电脑

    是否支持CI(持续集成)

    ….

    我们首先来看看XCode内置的框架:XCTest。XCTest又可以分为两部分:Unit Test 和 UI Test,分别对应单元测试和UI测试。有一些三方的测试库也是基于XCTest框架的,这个在后文会讲到。由于是Apple官方提供的,所以这个框架会不断完善。

    成熟的三方框架通常提供了很多封装好的有好的接口,笔者综合对比了一些,推荐以下框架:

    单元测试:

    以下三个框架都是BDD(Behavior-driven development) - 行为驱动开发。行为驱动开发简单来说就是先定义行为,然后定义测试用例,接着再编写代码。 实践中发现,通常没有那么多时间来先定义行为,不过BDD中的domain-specific language (DSL)能够很好的描述用例的行为。

    • Kiwi 老牌测试框架

    • specta 另一个BDD优秀框架

    • Quick 三个项目中Star最多,支持OC和Swift,优先推荐。

    UI测试

    • KIF 基于XCTest的测试框架,调用私有API来控制UI,测试用例用Objective C或Swift编写。

    • appium 基于Client - Server的测试框架。App相当于一个Server,测试代码相当于Client,通过发送JSON来操作APP,测试语言可以是任意的,支持android和iOS。

    篇幅有限,本文会先介绍XCtest,接着三方的Unit框架会以Quick为例,UI Test框架侧重分析KIF,appium仅仅做原理讲解。

    XCTest
    对于XCTest来说,最后生成的是一个bundle。bundle是不能直接执行的,必须依赖于一个宿主进程。关于XCTest进行单元测试的基础(XCode的使用,异步测试,性能测试,代码覆盖率等),我在这篇文章里讲解过,这里不再详细讲解。

    单元测试用例
    比如,我有以下一个函数:

    //验证一段Text是否有效。(不能以空字符开头,不能为空)
    - (BOOL)validText:(NSString *)text error:(NSError *__autoreleasing *)error{
    }
    

    那么,我该如何为这个函数编写单元测试的代码?通常,需要考虑以下用例:

    输入以空白字符或者换行符开头的,error不为空,返回 NO

    输入正确的内容,error为空,返回YES

    输入为nil,error不为空,返回 NO (边界条件)

    输入为非NSString类型,验证不通过,返回NO (错误输入)

    特殊输入字符(标点符号,非英文等等)

    UI测试
    UI测试是模拟用户操作,进而从业务处层面测试。关于XCTest的UI测试,建议看看WWDC 2015的这个视频:

    关于UI测试,有几个核心类需要掌握

    UI测试还有一个核心功能是UI Recording。选中一个UI测试用例,然后点击图中的小红点既可以开始UI Recoding。你会发现:

    随着点击模拟器,自动合成了测试代码。(通常自动合成代码后,还需要手动的去调整)


    image.png

    在写UI测试用例的时候要注意:测试行为而不是测试代码。比如,我们测试这样一个case

    进入Todo首页,点击add,进入添加页面,输入文字,点击save。

    测试效果如下:


    1491015312986175.gif

    对应测试代码:

    - (void)testAddNewItems{
        //获取app代理
        XCUIApplication *app = [[XCUIApplication alloc] init];
        //找到第一个tabeview,就是我们想要的tableview
        XCUIElement * table = [app.tables elementBoundByIndex:0];
        //记录下来添加之前的数量
        NSInteger oldCount = table.cells.count;
        //点击Add
        [app.navigationBars[@"ToDo"].buttons[@"Add"] tap];
        //找到Textfield
        XCUIElement *inputWhatYouWantTodoTextField = app.textFields[@"Input what you want todo"];
        //点击Textfield
        [inputWhatYouWantTodoTextField tap];
        //输入字符
        [inputWhatYouWantTodoTextField typeText:@"somethingtodo"];
        //点击保存
        [app.navigationBars[@"Add"].buttons[@"Save"] tap];
        //获取当前的数量
        NSInteger newCount = table.cells.count;
        //如果cells的数量加一,则认为测试成功
        XCTAssert(newCount == oldCount + 1);
    }
    

    这里是通过前后tableview的row数量来断言成功或者失败。

    等待

    通常,在视图切换的时候有转场动画,我们需要等待动画结束,然后才能继续,否则query的时候很可能找不到我们想要的控件。

    比如,如下代码等待VC转场结束,当query只有一个table的时候,才继续执行后续的代码。

    [self expectationForPredicate:[NSPredicate predicateWithFormat:@"self.count = 1"]
              evaluatedWithObject:app.tables
                          handler:nil];
    [self waitForExpectationsWithTimeout:2.0 handler:nil];
    //后续代码....
    

    Tips: 当你的UI结构比较复杂的时候,比如各种嵌套childViewController,使用XCUIElementQuery的代码会很长,也不好维护。

    另外,UI测试还会在每一步操作的时候截图,方便对测试报告进行验证。

    查看测试结果

    使用基于XCTest的框架,可以在XCode的report navigator中查看测试结果。

    其中:

    Tests 用来查看详细的测试过程

    Coverage 用来查看代码覆盖率

    Logs 用来查看测试的日志

    点击图中的红色框指向的图标可以看到每一步UI操作的截图

    除了利用XCode的GUI,还可以通过后文提到的命令行工具来测试,查看结果。

    Stub/Mock

    首先解释两个术语:

    mock 表示一个模拟对象

    stub 追踪方法的调用,在方法调用的时候返回指定的值。

    通常,如果你采用纯存的XCTest,推荐采用OCMock来实现mock和stub,单元测试的三方库通常已集成了stub和mock。

    那么,如何使用mock呢?举个官方的例子:

    //mock一个NSUserDefaults对象
    id userDefaultsMock = OCMClassMock([NSUserDefaults class]);
    //在调用stringForKey的时候,返回http://testurl
    OCMStub([userDefaultsMock 
    stringForKey:@"MyAppURLKey"]).andReturn(@"http://testurl");
    

    再比如,我们要测试打开其他App,那么如何判断确实打开了其他App呢?

    id app = OCMClassMock([UIApplication class]);
    OCMStub([app sharedInstance]).andReturn(app);
    OCMVerify([app openURL:url]
    

    使用Stub可以让我们很方便的实现这个。

    关于OCMock的使用,推荐看看objc.io的这篇文章

    Quick
    Quick是建立在XCTestSuite上的框架,使用XCTestSuite允许你动态创建测试用例。所以,使用Quick,你仍让可以使用XCode的测试相关GUI和命令行工具。

    使用Quick编写的测试用例看起来是这样子的:

    import Quick
    import Nimble
    
    class TableOfContentsSpec: QuickSpec {
      override func spec() {
        describe("the 'Documentation' directory") {
          it("has everything you need to get started") {
            let sections = Directory("Documentation").sections
            expect(sections).to(contain("Organized Tests with Quick Examples and Example Groups"))
            expect(sections).to(contain("Installing Quick"))
          }
          
          context("if it doesn't have what you're looking for") {
            it("needs to be updated") {
              let you = You(awesome: true)
              expect{you.submittedAnIssue}.toEventually(beTruthy())
            }
          }
        }
      }
    }
    

    BDD的框架让测试用例的目的更加明确,测试是否通过更加清晰。使用Quick,测试用例分为两种:

    单独的用例 - 使用it来描述

    it有两个参数,

    行为描述

    行为的测试代码

    比如,以下测试Dolphin行为,它具有行为is friendly和is smart

    //Swift代码
    class DolphinSpec: QuickSpec {
      override func spec() {
        it("is friendly") {
          expect(Dolphin().isFriendly).to(beTruthy())
        }
        
        it("is smart") {
          expect(Dolphin().isSmart).to(beTruthy())
        }
      }
    }
    

    可以看到,BDD的核心是行为。也就是说,需要关注的是一个类提供哪些行为。

    用例集合,用describe和context描述

    比如,验证dolphin的click行为的时候,我们需要两个用例。一个是is loud,一个是has a high frequency,就可以用describe将用例组织起来。

    class DolphinSpec: QuickSpec {
      override func spec() {
        describe("a dolphin") {
          describe("its click") {
            it("is loud") {
              let click = Dolphin().click()
              expect(click.isLoud).to(beTruthy())
            }
            
            it("has a high frequency") {
              let click = Dolphin().click()
              expect(click.hasHighFrequency).to(beTruthy())
            }
          }
        }
      }
    }
    

    context可以指定用例的条件:

    比如:

    describe("its click") {
        context("when the dolphin is not near anything interesting") {
          it("is only emitted once") {
            expect(dolphin!.click().count).to(equal(1))
          }
        }
    }
    

    除了这些之外,Quick也支持一些切入点,进行测试前的配置:

    beforeEach

    afterEach

    beforeAll

    afterAll

    beforeSuite

    afterSuite

    Nimble

    由于Quick是基于XCTest,开发者当然可以收使用断言来定义测试用例成功或者失败。Quick提供了一个更有好的Framework来进行这种断言:Nimble

    比如,一个常见的XCTest断言如下:

    XCTAssertTrue(ConditionCode, "FailReason")11
    

    在出错的时候,会提示

    XCAssertTrue failed, balabala
    

    这时候,开发者要打个断点,查看下上下文,看看具体失败的原因在哪。
    使用Nimble后,断言变成类似

    expect(1 + 1).to(equal(2))
    expect(3) > 2
    expect("seahorse").to(contain("sea"))
    expect(["Atlantic", "Pacific"]).toNot(contain("Mississippi"))
    

    并且,出错的时候,提示信息会带着上下文的值信息,让开发者更容易的找到错误。

    让你的代码更容易单元测试

    测试的准确性和工作量很大程度上依赖于开发人员的代码质量。

    通常,为了单元测试的准确性,我们在写函数(方法)的时候会借鉴一些函数式编程的思想。其中最重要的一个思想就是

    pure function(纯函数)

    何为Pure function?就是如果一个函数的输入一样,那么输出一定一样。

    比如,这样的一个函数就不是pure function。因为它依赖于外部变量value的值。

    static NSInteger value = 0;
    - (NSInteger)function_1{
        value = value + 1;
        return value;
    }
    

    而这个函数就是pure function,因为给定输入,输出一定一致。

    - (NSInteger)function_2:(NSInteger)base{
        NSInteger value = base + 1;
        return value;
    }
    

    所以,如果你写了一个没有参数,或者没有返回值的方法,那么你要小心了,很可能这个方法很难测试。

    关于MVC

    在良好的MVC架构的App中,

    • View只做纯粹的展示型工作,把用户交互通过各种方式传递到外部

    • Model只做数据存储类工作

    • Controller作为View和Model的枢纽,往往要和很多View和Model进行交互,也是自动化包括代码维护的痛点。

    所以,对Controller瘦身是iOS架构中比较重要的一环,一些通用的技巧包括:

    逻辑抽离:

    • 网络请求独立。可以每个网络请求以Command模式封装成一个对象,不要直接在Controller调用AFNetworking。

    • 数据存储独立。建立独立的Store类,用来做数据持久化和缓存。

    • 共有数据服务化(协议)。比如登录状态等等,通过服务去访问,这样服务提供者之需要处理服务的质量,服务使用者则信任服务提供者的结果。

    Controller与View解耦合

    • 建立ViewModel层,这样Controller只需要和ViewModel进行交互。

    • 建立UIView子类作为容器,将一些View放到容器后再把容器作为SubView添加到Controller里

    • 建立可复用的Layout层,不管是AutoLayout还是手动布局。

    Controller与Controller解耦合

    • 建立页面路由。每一个界面都抽象为一个URL,跳转仅仅通过Intent或者URL跳转,这样两个Controller完全独立。

    如果你的App用Swift开发,那么面向协议编程和不可变的值类型会让你的代码更容易测试。

    当然,iOS组建化对自动化测试的帮助也很大,因为不管是基础组件还是业务组件,都可以独立测试。组建化又是一个很大的课题,这里不深入讲解了。

    KIF

    KIF的全称是Keep it functional。它是一个建立在XCTest的UI测试框架,通过accessibility来定位具体的控件,再利用私有的API来操作UI。由于是建立在XCTest上的,所以你可以完美的借助XCode的测试相关工具(包括命令行脚本)。

    > KIF是个人非常推荐的一个框架,简单易用。

    使用KIF框架强制要求你的代码支持accessibility。如果你之前没接触过,可以看看Apple的文档

    简单来说,accessibility能够让视觉障碍人士使用你的App。每一个控件都有一个描述AccessibilityLabel。在开启VoiceOver的时候,点击控件就可以选中并且听到对应的描述。

    通常UIKit的控件是支持accessibility的,自定定义控件可以通过代码或者Storyboard上设置。

    在Storyboard上设置:


    image.png

    上面的通过Runtime Attributes设置(KVC)

    下面的通过GUI来设置

    通过代码设置:

    [alert setAccessibilityLabel:@"Label"];
    [alert setAccessibilityValue:@"Value"];
    [alert setAccessibilityTraits:UIAccessibilityTraitButton];
    

    如果你有些Accessibility的经验,那么你肯定知道,像TableView的这种不应该支持VoiceOver的。我们可以用条件编译来只对测试Target进行设置:

    #ifdef DEBUG
    [tableView setAccessibilityValue:@"Main List Table"];
    #endif
    
    #ifdef KIF_TARGET (这个值需要在build settings里设置)
    [tableView setAccessibilityValue:@"Main List Table"];
    #endif
    

    使用KIF主要有两个核心类:

    KIFTestCase XCTestCase的子类

    KIFUITestActor 控制UI,常见的三种是:点击一个View,向一个View输入内容,等待一个View的出现

    我们用KIF来测试添加一个新的ToDo

    - (void)testAddANewItem{
        [tester tapViewWithAccessibilityLabel:@"Add"];
        [tester enterText:@"Create a test to do item" intoViewWithAccessibilityLabel:@"Input what you want todo"];
        [tester tapViewWithAccessibilityLabel:@"Save"];
        [tester waitForTimeInterval:0.2];
        [tester waitForViewWithAccessibilityLabel:@"Create a test to do item"];
    }
    

    命令行

    自动化测试中,命令行工具可以facebook的开源项目:

    这是一个基于xcodebuild命令的扩展,在iOS自动化测试和持续集成领域很有用,而且它支持-parallelize并行测试多个bundle,大大提高测试效率。

    安装XCTool,

    brew install xctool11
    

    使用:

    path/to/xctool.sh \
      -workspace YourWorkspace.xcworkspace \
      -scheme YourScheme \
      -reporter plain:/path/to/plain-output.txt \
      run-test
    

    并且,xctool对于持续集成很有用,iOS常用的持续集成的server有两个:

    • Travis CI 对于公开仓库(比如github)免费,私有仓库收费

    • Jenkins 免费

    优化你的测试代码

    准确的测试用例

    通常,你的你的测试用例分为三部分:

    • 配置测试的初始状态

    • 对要测试的目标执行代码

    • 对测试结果进行断言(成功 or 失败)

    测试代码结构

    当测试用例多了,你会发现测试代码编写和维护也是一个技术活。通常,我们会从几个角度考虑:

    • 不要测试私有方法(封装是OOP的核心思想之一,不要为了测试破坏封装)

    • 对用例分组(功能,业务相似)

    • 对单个用例保证测试独立(不受之前测试的影响,不影响之后的测试),这也是测试是否准确的核心。

    • 提取公共的代码和操作,减少copy/paste这类工作,测试用例是上层调用,只关心业务逻辑,不关心内部代码实现。

    一个常见的测试代码组织如下:

    image.png
    appium
    appium采用了Client Server的模式。对于App来说就是一个Server,基于WebDriver JSON wire protocol对实际的UI操作库进行了封装,并且暴露出RESTFUL的接口。然后测试代码通过HTTP请求的方式,来进行实际的测试。其中,实际驱动UI的框架根据系统版本有所不同:
    • < 9.3 采用UIAutomation

    • = 9.3 XCUITest

    原因也比较简单:Apple在10.0之后,移除了UIAutomation的支持,只支持XCUITest。


    image.png

    对比KIF,appium有它的优点:

    • 跨平台,支持iOS,Android

    • 测试代码可以由多种语言编写,这对测试来说门槛更低

    • 测试脚本独立与源代码和测试框架

    当然,任何框架都有缺点:

    • 自定义控件支持不好

    • WebView的支持不好

    总结

    由于我不是专业的iOS测试,关于测试的一点见解如下:

    单元测试还是选择BDD框架,毕竟可读性高一些,推荐Quick(Swift),Kiwi(Objective C)

    UI测试优先推荐KIF,如果需要兼顾安卓测试,或者测试人员对OC/Swift很陌生,可以采用appium

    参考资料

    相关文章

      网友评论

          本文标题:iOS自动化测试(1)

          本文链接:https://www.haomeiwen.com/subject/xyqehctx.html