自动化Test使用详细解析(二) —— 单元测试和UI Test使用简单示例(一)

版本记录

版本号 时间
V1.0 2019.04.19 星期五

前言

自动化Test可以通过编写代码、或者是记录开发者的操作过程并代码化,来实现自动化测试等功能。接下来几篇我们就说一下该技术的使用。感兴趣的可以看下面几篇。
1. 自动化Test使用详细解析(一) —— 基本使用(一)

开始

首先看下写作环境

Swift 4.2, iOS 12, Xcode 10

本篇主要了解如何将单元测试和UI测试添加到iOS应用程序,以及如何检查代码覆盖率。

写作测试不是很迷人,但是由于测试可以防止让你的闪亮应用程序变成了一个充满bug的垃圾,这是必要的。 如果您正在阅读本教程,您已经知道您应该为您的代码和UI编写测试,但您可能不知道如何去做。

您可能有一个有效的应用程序,但您想测试您为扩展应用程序所做的更改。 也许您已经编写了测试,但不确定它们是否是正确的测试。 或者,您已经开始研究新的应用程序,并希望随时进行测试。

本教程将向您展示:

  • 如何使用Xcode的Test导航器测试应用程序的模型和异步方法
  • 如何使用stubs and mocks与库或系统对象的交互
  • 如何测试UI和性能
  • 如何使用代码覆盖工具

在此过程中,您将获得测试ninjas所使用的一些词汇。


Figuring Out What to Test

在编写任何测试之前,了解基础知识非常重要。 你需要测试什么?

如果您的目标是扩展现有应用程序,则应首先为计划更改的任何组件编写测试。

通常,测试应包括:

  • 核心功能:模型类和方法及其与控制器的交互
  • 最常见的UI工作流程
  • 边界条件
  • Bug修复

1. Best Practices for Testing

首字母缩略词FIRST描述了有效单元测试的一套简明标准。 这些标准是:

  • Fast - 快速:测试应该快速进行。
  • Independent/Isolated - 独立/隔离:测试不应彼此共享状态。
  • Repeatable - 可重复:每次运行测试时都应获得相同的结果。 外部数据提供者或并发问题可能导致间歇性故障。
  • Self-validating - 自我验证:测试应完全自动化。 输出应该是“通过”或“失败”,而不是依赖于程序员对日志文件的解释。
  • Timely - 及时:理想情况下,应在编写测试的生产代码(测试驱动开发)之前编写测试。

遵循FIRST原则将使您的测试保持清晰且有用,而不是为您的应用程序设置障碍。

打开已经下载的工程文件:

  • BullsEye基于iOS Apprentice中的示例应用程序。 游戏逻辑位于BullsEyeGame类中,您将在本教程中测试它。
  • HalfTunesURLSession教程中示例应用程序的更新版本。 用户可以在iTunes API中查询歌曲,然后下载和播放歌曲片段。

Unit Testing in Xcode

Test navigator提供了最简单的测试方法,您将使用它来创建测试目标并针对您的应用程序运行测试。

1. Creating a Unit Test Target

打开BullsEye项目并按Command-6打开Test navigator

单击左下角的+按钮,然后从菜单中选择New Unit Test Target ...

接受默认名称BullsEyeTests。 当test bundle出现在Test navigator中时,单击以在编辑器中打开该包。 如果bundle未自动显示,请单击其他导航器之一进行故障排除,然后返回Test navigator

默认模板导入测试框架XCTest,并使用setUp()tearDown()和示例测试方法定义XCTestCaseBullsEyeTests子类。

运行测试有三种方法:

  • 1) Product ▸ Test or Command-U。 这两个都运行所有测试类。
  • 2) 单击Test navigator中的箭头按钮。
  • 3) 单击gutter中的菱形按钮。

您还可以通过在Test navigatorgutter中单击其菱形来运行单个测试方法。

尝试不同的方式来运行测试,以了解它需要多长时间以及它看起来像什么。 样本测试还没有做任何事情,所以它们运行得非常快!

当所有测试成功后,菱形将变为绿色并显示复选标记。 您可以单击testPerformanceExample()末尾的灰色菱形以打开性能结果:

本教程不需要testPerformanceExample()testExample(),因此请删除它们。

2. Using XCTAssert to Test Models

首先,您将使用XCTAssert函数来测试BullsEye模型的核心功能:BullsEyeGame对象是否正确计算了一轮的分数?

BullsEyeTests.swift中,在import语句下方添加以下行:

@testable import BullsEye

这使得单元测试可以访问BullsEye中的internal类型和功能。

BullsEyeTests类的顶部,添加以下属性:

var sut: BullsEyeGame!

这为BullsEyeGame创建了一个占位符,它是System Under Test (SUT),或者是测试用例类与测试有关的对象。

接下来,用这个替换setup()的内容:

super.setUp()
sut = BullsEyeGame()
sut.startNewGame()

这会在类级别创建BullsEyeGame对象,因此此测试类中的所有测试都可以访问SUT对象的属性和方法。

在这里,您还可以调用游戏的startNewGame()来初始化targetValue。 许多测试将使用targetValue来测试游戏是否正确计算得分。

在您忘记之前,请在tearDown()中释放您的SUT对象。 将其内容替换为:

sut = nil
super.tearDown()

注意:最好在setUp()中创建SUT并在tearDown()中释放它,以确保每个测试都以一个干净的平板开始。 有关更多讨论,请查看Jon Reid’s post关于此主题的帖子。


Writing Your First Test

现在,您已经准备好编写第一个测试了!

将以下代码添加到BullsEyeTests的末尾:

func testScoreIsComputed() {
  // 1. given
  let guess = sut.targetValue + 5

  // 2. when
  sut.check(guess: guess)

  // 3. then
  XCTAssertEqual(sut.scoreRound, 95, "Score computed from guess is wrong")
}

测试方法的名称始终以test开头,然后是对其测试内容的描述。

将测试格式化为given, when and then的部分是一种很好的做法:

  • Given:在这里,您可以设置所需的任何值。 在此示例中,您将创建一个guess值,以便指定它与targetValue的差异。
  • When:在本节中,您将执行正在测试的代码:调用check(guess :)
  • Then:这是您将断言您期望的结果的部分,如果测试失败则打印一条消息。 在这种情况下,sut.scoreRound应该等于95(100 - 5)

单击gutterTest navigator中的菱形图标运行测试。 这将构建并运行应用程序,菱形图标将变为绿色复选标记!

注意:要查看XCTestAssertions的完整列表,请转到 Apple’s Assertions Listed by Category

1. Debugging a Test

BullsEyeGame故意内置了一个错误,你现在就可以练习找到它。 要查看操作中的bug,您将创建一个测试,从given部分中的targetValue中减去5,并使其他所有内容保持不变。

添加以下测试:

func testScoreIsComputedWhenGuessLTTarget() {
  // 1. given
  let guess = sut.targetValue - 5

  // 2. when
  sut.check(guess: guess)

  // 3. then
  XCTAssertEqual(sut.scoreRound, 95, "Score computed from guess is wrong")
}

guesstargetValue之间的差异仍为5,因此得分仍应为95

在“断点”导航器Breakpoint navigator中,添加Test Failure Breakpoint。 当测试方法发布失败assertion时,这将停止测试运行。

运行测试,它应该在测试失败时停在XCTAssertEqual行。

在调试控制台中检查sutguess

guesstargetValue - 5但是scoreRound105,而不是95

要进一步调查,请使用正常的调试过程:在when语句中设置一个断点,在BullsEyeGame.swift中设置一个断点,在check(guess :)中,它会产生difference。 然后再次运行测试,并跳过let difference语句以检查应用程序中difference的值:

问题是difference是负值,所以得分是100 - ( - 5)。 要解决此问题,您应该使用difference的绝对值。 在check(guess :)中,取消注释正确的行并删除不正确的行。

删除两个断点,然后再次运行测试以确认它现在成功。

2. Using XCTestExpectation to Test Asynchronous Operations

现在您已经学会了如何测试模型和调试测试失败,现在是时候继续测试异步代码了。

打开HalfTunes项目。 它使用URLSession来查询iTunes API并下载歌曲样本。 假设您要修改它以使用AlamoFire进行网络操作。 要查看是否有任何中断,您应该为网络操作编写测试并在更改代码之前和之后运行它们。

URLSession方法是异步的:它们立即返回,但直到稍后才完成运行。 要测试异步方法,请使用XCTestExpectation使测试等待异步操作完成。

异步测试通常很慢,因此您应该将它们与更快的单元测试分开。

创建一个名为HalfTunesSlowTests的新单元测试目标。 打开HalfTunesSlowTests类,并在现有import语句下方导入HalfTunes应用程序模块:

@testable import HalfTunes

此类中的所有测试都使用默认的URLSession将请求发送到Apple的服务器,因此声明一个sut对象,在setUp()中创建它并在tearDown()中释放它。

用以下内容替换HalfTunesSlowTests类的内容:

var sut: URLSession!

override func setUp() {
  super.setUp()
  sut = URLSession(configuration: .default)
}

override func tearDown() {
  sut = nil
  super.tearDown()
}

下面,添加异步测试:

// Asynchronous test: success fast, failure slow
func testValidCallToiTunesGetsHTTPStatusCode200() {
  // given
  let url = 
    URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba")
  // 1
  let promise = expectation(description: "Status code: 200")

  // when
  let dataTask = sut.dataTask(with: url!) { data, response, error in
    // then
    if let error = error {
      XCTFail("Error: \(error.localizedDescription)")
      return
    } else if let statusCode = (response as? HTTPURLResponse)?.statusCode {
      if statusCode == 200 {
        // 2
        promise.fulfill()
      } else {
        XCTFail("Status code: \(statusCode)")
      }
    }
  }
  dataTask.resume()
  // 3
  wait(for: [promise], timeout: 5)
}

此测试检查向iTunes发送有效查询是否返回200状态代码。 大多数代码与您在应用程序中编写的代码相同,使用以下附加行:

  • 1) expectation(description :):返回存储在promise中的XCTestExpectation对象。 description参数描述了您期望发生的事情。
  • 2) promise.fulfill():在异步方法的完成处理程序的成功条件闭包中调用它来标记已满足期望。
  • 3) wait(for:timeout :):保持测试运行,直到满足所有期望,或超时间隔timeout结束,以先发生者为准。

运行测试。 如果您已连接到互联网,则应用程序在模拟器中加载后,测试应该需要大约一秒钟才能成功。

3. Failing Fast

失败会伤害,但它不需要永远。

要体验失败,只需从URL中的“itunes”中删除's'即可:

let url = 
  URL(string: "https://itune.apple.com/search?media=music&entity=song&term=abba")

运行测试。 它失败了,但需要完整的超时间隔! 这是因为你假设请求总是成功,那就是你调用promise.fulfill()的地方。 由于请求失败,仅在超时到期时才完成。

您可以通过更改以下假设来改进此问题并使测试失败:不要等待请求成功,而是等待直到调用异步方法的完成处理程序。 一旦应用程序收到来自服务器的响应(OK或错误),就会发生这种情况,这符合预期。 然后,您的测试可以检查请求是否成功。

要了解其工作原理,请创建一个新测试。

但首先,通过撤消对url所做的更改来修复上一个测试。

然后,将以下测试添加到您的类:

func testCallToiTunesCompletes() {
  // given
  let url = 
    URL(string: "https://itune.apple.com/search?media=music&entity=song&term=abba")
  let promise = expectation(description: "Completion handler invoked")
  var statusCode: Int?
  var responseError: Error?

  // when
  let dataTask = sut.dataTask(with: url!) { data, response, error in
    statusCode = (response as? HTTPURLResponse)?.statusCode
    responseError = error
    promise.fulfill()
  }
  dataTask.resume()
  wait(for: [promise], timeout: 5)

  // then
  XCTAssertNil(responseError)
  XCTAssertEqual(statusCode, 200)
}

关键的区别在于,简单地输入完成处理程序就能满足期望,而这只需要大约一秒钟的时间。 如果请求失败,则then断言失败。

运行测试。 它现在应该花费大约一秒钟才能失败。 它失败是因为请求失败,而不是因为测试运行超过了timeout

修复url,然后再次运行测试以确认它现在成功。

后记

本篇主要介绍了单元测试和UI Test使用简单示例,感兴趣的给个赞或者关注~~~

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

推荐阅读更多精彩内容