iOS之性能优化iOS之开发配置

iOS自动化测试的那些干货

2017-05-13  本文已影响687人  伯牙呀

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


流程

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

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

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

本文所讲解的均是基于XCode 8.2.1,有些概念可能不适用于低版本的XCode。

1、自动化测试

自动化测试就是写一些测试代码,用代码代替人工去完成模块和业务的测试。

其实不管是开发还是测试,如果你在不断的做重复性工作的时候,就应该问自己一个问题:是不是有更高效的办法?

自动化测试有很多优点:

当然,自动化测试也有一些缺点:

所以,在做自动化测试之前,首先要问自己几个问题?

通常,我们会选择那些业务稳定,需要频繁测试的部分来编写自动化测试脚本,其余的采用人工测试,人工测试仍然是iOS App开发中不可缺少的一部分。

2、测试种类

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

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

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

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


测试金字塔

而iOS测试通常只有以下两个层次:

3、框架选择

啰里八嗦讲的这么多,自动化测试的效率怎么样,关键还是在测试框架上。那么,如何选择测试框架呢?

框架可以分为两大类:XCode内置的和三方库。

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

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

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

单元测试:

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

UI测试:

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

4、XCTest

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

单元测试用例:

比如,我有以下一个函数:

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

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

UI测试:

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

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

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

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


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

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

测试效果如下:



对应测试代码:

- (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中查看测试结果。



其中:

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

Stub/Mock:

首先解释两个术语:

通常,如果你采用纯存的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的这篇文章:

5、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 friendlyis 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也支持一些切入点,进行测试前的配置:

Nimble

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

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

XCTAssertTrue(ConditionCode, "FailReason")

在出错的时候,会提示:

XCAssertTrue failed, balabala

这时候,开发者要打个断点,查看下上下文,看看具体失败的原因在哪。

使用Nimble后,断言变成类似:

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

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

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

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

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

何为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中:

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

逻辑抽离:

Controller与View解耦合:

Controller与Controller解耦合:

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

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

7、KIF

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

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

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

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

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


在Storyboard上设置

通过代码设置:

[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主要有两个核心类:

我们用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"];
}

8、命令行

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

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

安装XCTool:

brew install xctool

使用:

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

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

9、优化你的测试代码

准确的测试用例:

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

测试代码结构:

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

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


测试代码组织

10、appium

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

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

对比 KIF,appium 有它的优点:

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

总结

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


参考资料

转载出处:iOS自动化测试的那些干货

上一篇 下一篇

猜你喜欢

热点阅读