Xcode7 中用 Swift 做单元测试

作者:Maxime Defauw,原文链接,原文日期:2016-02-29
译者:ray;校对:numbbbbb;定稿:way

每个 iOS 程序员都要时不时的为他们的 app 做 debug。除非你是那种超级大牛,否则你肯定体验过查了无数个小时的 bug 最后才发现那仅仅是个简单的语法错误时那种油然而生的绝望感。或者更糟:你根本就没发现那些 bug。无论你是编程新手,还是开发过很多 app 的老司机,例行的写写单元测试会让你的代码更可靠,更安全,更容易 debug!

你很走运,Xcode 7 和 Swift 支持单元测试。尽管单元测试不保证(有了它你就会写出)绝对没有 bug 的 app,它还是一种能让你验证每段代码是否如期工作,并让 debug 过程更加便利。

正如其名,在单元测试中你要为某段代码单元创建一些小规模的、针对其某个特性的测试,然后确保每个代码单元都能通过这些测试。如果通过的话,它的旁边会出现一个绿色小标志,而如果因故测试不通过, Xcode 会把该测试标记为 "failed"。这就提示你去查看代码,找出失败原因。

演示项目概览

首先下载这个我为你准备的 starting project。一个短小精悍的 app:它会对一个给定的数字和百分比做一个乘法计算。(比如80的10%是8。)

这个 PercentageCalculator 项目非常简单。你唯一需要关注的就是 ViewController.swift 这个文件。里面的代码都标记了注释,很容易理解。

有 5 个 IBOutlets:每一个都对应了屏幕上一个 UIElement,除 title(标题)之外,还有 2 个 slider 对应 2 个 IBActions。每个 IBAction 的方法名都精确描述了其用途及将要执行的操作。当一个 slider 值改变时,其对应着的百分比或数字的值也会随之改变。

还有两个简单的函数 “updateLabels()” 和 “percentage()” 做了符合期待的事情:当一个 slider 改变时第一个函数更新 label,第二个函数获取两个浮点数并返回百分比的计算结果。

在模拟器中运行 app。刚开始一切看起来都很正常。但当你开始改变数字时就会发现计算结果有问题。为找到 bug,我们将代码分割成不同的单元,然后分别做测试,看看每个是否都如期运行。这不会解决 bug,但能缩小你的查找范围。

我创建项目的时候,默认情况下会勾选创建一个 test 文件的选项(如果你想要手动加一个的话,在 iOS Source 下面选择 select File > New > File > Unit Test Case Class)。我们的例子中 test 文件已经被 Xcode 自动创建出来,可以在项目导航栏中 “PercentageCalculatorTests” 文件夹中找到它。

PercentageCalculatorTests.swift 文件中,PercentageCalculatorTests 类里面已经为我们创建好了 4 个方法。其中 2 个是测试方法(test methods)的例子,你可以删掉它们(它俩都以 test 关键字开头,并且它们左边的竖条中都有个方块形图标,名字也都以 “...Example” 结尾,所以你可以通过这些辨识出来它们是测试方法)。另外两个方法,setUp()tearDown() 是特殊的样板方法(boilerplate methods),它们分别在每个测试方法被执行之前,和每个测试方法被执行之后被执行。

开始写单元测试吧

现在是时候写你的第一个单元测试函数了!本教程我们只测试 ViewController 类,需要在 PercentageCalculatorTests 中添加一个它的实例。

class PercentageCalculatorTests: XCTestCase {
    var vc: ViewController!
    
    override func setUp() {
        super.setUp()
        // 这里写setup的代码。本class里每个测试函数被调用之前该方法都会被先调用。
    }
    
    override func tearDown() {
        // 这里写teardown的代码。本class里每个测试函数被调用之后该方法都会被调用。
        super.tearDown()
    }
    
}

PercentageCalculatorTests 是一个 XCTestCase 的子类,后者被打包在 XCTest 框架中。每一个 XCTestCase 子类的实例都负责对你项目的某个特定部分做测试,比如对一个特性做测试。

在 setup 方法中实例化一个 vc。这样对每一个测试方法你都会得到一个“全新的” ViewController 实例,因为在每个测试方法执行前 setUp() 都会被调用一次。把 setUp() 方法修改如下:

override func setUp() {
    super.setUp()
 
    let storyboard = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle())
    vc = storyboard.instantiateInitialViewController() as! ViewController
}

现在你应该记得所有的测试方法的名字都要以 test 关键字开头,否则 Xcode 不会识别。添加一个新的 testPercentageCalculator() 测试方法,来验证一下 ViewController 中的 percentage() 工作是否正常。

func testPercentageCalculator() {
}

单元测试中你要去检查某段代码是否如你所愿的那样工作。待测试的代码段一般都只有几行,典型情况是你只需要测试一个方法或者一个函数。单元测试是这样去做的:你给某个代码单元一个输入值,让这个值过一遍这段代码,然后检查一下输出的值是否和预期的一样。

与“我们期望的那个值”做比较的这部分由 XCTAssert 函数来处理。最简单的 XCTAssert 函数是XCTAssert(expression: BooleanType)。这个函数要求一个布尔表达式(类似于 5>38.90 == 8.90或者 true 这种),随后如果表达式为真则让测试通过,否则认为测试失败。

尝试一下!首先给 testPercentageCalculator() 方法加添加下面一行。然后把光标移到方法名左边侧栏的那个方块图标上,停下光标之后方块图标变成了一个执行光标,点击一下就开始了测试。

func testPercentageCalculator() {
        XCTAssert(true)
}

如果一切顺利,则测试通过,方法左边会出现一个绿色检测标。

验证百分比计算

现在来真的:测试 percentage() 方法!用 ViewController 的一个实例 - vc 属性来调用这个方法。给这个方法两个浮点数,比如 50 和 50,然后把结果存储到常量 p 中。这个例子中 p 应该是 25(50 的 50% 是 25)。然后用 XCTAssert(p == 25) 检测一下是不是这样,执行测试方法。把 testPercentageCalculator() 改成这样:

func testPercentageCalculator() {
        // 应该是25
        let p = vc.percentage(50, 50)
        XCTAssert(p == 25)
}

测试成功了,这意味着 ViewControllerpercentage() 函数工作正常,我们应该在其他的地方继续寻找 bug。也许 bug 在 updateLabels() 里面?

验证Labels

现在添加一个新的测试方法 testLabelValuesShowedProperly() 来验证一下 label 能不能正确的显示 text。和之前一样,调用 ViewController 的一个方法 - 这回是 updateLabels() - 然后看看每个标签的 text 属性和我们期望的那个 text 是否相同。

注意到你要给 XCTAssert 函数传一个新的参数:一个 string 类型的消息。这对我们这次要对多个值做检查(调用三次 XCTAssert )来完成测试而言就会很方便。如果测试失败,这条消息就会指名我们具体是哪里错了。

func testLabelValuesShowedProperly() {
        vc.updateLabels(Float(80.0), Float(50.0), Float(40.0))
        
        // labels应该显示80, 50 and 40
        XCTAssert(vc.numberLabel.text == "80.0", "numberLabel doesn't show the right text")
        XCTAssert(vc.percentageLabel.text == "50.0%", "percentageLabel doesn't show the right text")
        XCTAssert(vc.resultLabel.text == "40.0", "resultLabel doesn't show the right text")
}

你尝试执行这个测试方法时,会收到编译器的错误提示:numberLabelpercentageLabelresultsLabelnil。怎么回事呢?

我是在 storyboard 文件中创建了这些 labels 的,因此只有当 view 被加载之后(loaded)它们才会被初始化,然而由于对单元测试来说 loadView() 方法不会被触发,所以这些 labels 没有被创建,只能是 nil。一种可能的方法是通过调用 vc.loadView() 来解决,但是 Apple 在它的文档中并不推荐你这么做,因为当已经被加载的对象又被加载一次的话可能会引起内存泄露。

正确的方法是你应该先访问一下 vcview 这个属性,这会让 vc 反过来触发所有相应的方法,不仅仅包括 loadView()。把 testLabelValuesShowedProperly() 改成这样:

func testLabelValuesShowedProperly() {
        let _ = vc.view
        vc.updateLabels(Float(80.0), Float(50.0), Float(40.0))
        
        // labels应该显示80, 50 and 40
        XCTAssert(vc.numberLabel.text == "80.0", "numberLabel doesn't show the right text")
        XCTAssert(vc.percentageLabel.text == "50.0%", "percentageLabel doesn't show the right text")
        XCTAssert(vc.resultLabel.text == "40.0", "resultLabel doesn't show the right text")
}

注意到下划线(_)忽略了常量的名字。因为我们实际上并不需要用到这个 view。加下划线就是告诉编译器“你假装访问一下这个 view,把相应的方法触发就行。”

执行测试。(如果想一并执行我们test类的所有测试,你还可以点击 “class PercentageCalculatorTests” 旁边的那个方块)。

我们来修Bug

如你所见,测试失败了!我们给 XCTAssert 方法传入的错误细节消息帮助我们快速识别出引起 bug 的可能原因。这次测试告诉我们 resultsLabel 没有显示出正确的文本,所以我们进到 ViewController 里看看对这些 label 的 text 值是在那里被设置的。仔细看了 ViewController.swiftupdateLabels() 代码之后,我们发现了 bug 的原因:

self.resultLabel.text = "\(rV + 10)"

应该是:

self.resultLabel.text = "\(rV)"

更新代码之后再运行一次测试,一切都应该正常了!

结论

本篇教程中你学到了 Xcode 中的单元测试的相关内容,以及它怎样能够帮你找到代码中的 bug。除了预防 bug 之外,单元测试还可以用来做性能测试和异步测试。还可能让你感兴趣的是UI测试,你可以录制下你在 app 上做出的动作来测试你的 app 在实际使用情景下是如何表现的。如果听起来觉得感兴趣,那一定要看看这个讲 UI 测试的 WWDC视频

项目的最终版本可以在 Github上下载

如果你有关于 UI 测试的任何问题,或者学习本教程中遇到了困难,请在评论中点我!

作者介绍:Maxime Defauw 是一个有经验的程序员,在 App Store 和 Google Play store 上发布过多个 app。他今年 16 岁,居住在比利时。最近他在 San Francisco 举行的 WWDC15 上获得了 Apple 的奖学金。Max 熟练掌握 Objective-C,C,C#,现在是 Swift。不码代码的时候他一般在曲棍球场或者高尔夫球场上。在 Twitter 上 @MaximeDefauw 粉他。

本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 http://swift.gg

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,423评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,147评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,019评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,443评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,535评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,798评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,941评论 3 407
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,704评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,152评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,494评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,629评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,295评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,901评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,742评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,978评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,333评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,499评论 2 348

推荐阅读更多精彩内容