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

版本记录

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

前言

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

Faking Objects and Interactions

异步测试使您确信您的代码会为异步API生成正确的输入。您可能还希望在从URLSession接收输入时测试您的代码是否正常工作,或者它是否正确更新了用户的默认数据库或iCloud容器。

大多数应用程序与系统或库对象(您无法控制的对象)进行交互,与这些对象交互的测试可能很慢且不可重复,违反了两个FIRST原则。相反,您可以通过从stubs获取输入或通过更新模拟mock对象来伪造交互。

当代码依赖于系统或库对象时,请使用伪造。您可以通过创建一个假对象来播放该部分并将此伪注入您的代码中来实现此目的。 Jon ReidDependency Injection描述了几种方法。

1. Fake Input From Stub

在此测试中,您将通过检查searchResults.count是否正确来检查应用程序的updateSearchResults(_ :)是否正确解析会话下载的数据。 SUT是视图控制器,您将使用stubs和一些预先下载的数据伪造会话。

转到Test navigator并添加新的Unit Test Target。将其命名为HalfTunesFakeTests。打开HalfTunesFakeTests.swift并导入import语句正下方的HalfTunes应用程序模块:

@testable import HalfTunes

现在,用以下内容替换HalfTunesFakeTests类的内容:

var sut: SearchViewController!

override func setUp() {
  super.setUp()
  sut = UIStoryboard(name: "Main", bundle: nil)
    .instantiateInitialViewController() as? SearchViewController
}

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

这声明了SUT,它是一个SearchViewController,在setUp()中创建它并在tearDown()中释放它:

注意:SUT是视图控制器,因为HalfTunes有一个巨大的视图控制器问题 - 所有工作都在SearchViewController.swift中完成。 Moving the networking code into a separate module可以减少此问题,并且还可以使测试更容易。

接下来,您将需要一些示例JSON数据,您的假会话将为您的测试提供这些数据。 只需要几个项目,所以要限制你的下载结果在iTunes中附加&limit = 3URL字符串:

https://itunes.apple.com/search?media=music&entity=song&term=abba&limit=3

复制此URL并将其粘贴到浏览器中。 这将下载名为1.txt1.txt.js或类似文件的文件。 预览它以确认它是一个JSON文件,然后将其重命名为abbaData.json

现在,返回Xcode并转到Project navigator。 将文件添加到HalfTunesFakeTests组。

HalfTunes项目包含支持文件DHURLSessionMock.swift。 这定义了一个名为DHURLSession的简单协议,其方法(stubs)用于创建具有URLURLRequest的数据任务。 它还定义了URLSessionMock,它符合此协议的初始化程序,允许您使用您选择的数据,响应和错误创建模拟URLSession对象。

要设置fake,请转到HalfTunesFakeTests.swift并在创建SUT的语句之后在setUp()中添加以下内容:

let testBundle = Bundle(for: type(of: self))
let path = testBundle.path(forResource: "abbaData", ofType: "json")
let data = try? Data(contentsOf: URL(fileURLWithPath: path!), options: .alwaysMapped)

let url = 
  URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba")
let urlResponse = HTTPURLResponse(
  url: url!, 
  statusCode: 200, 
  httpVersion: nil, 
  headerFields: nil)

let sessionMock = URLSessionMock(data: data, response: urlResponse, error: nil)
sut.defaultSession = sessionMock

这将设置虚假数据和响应并创建虚假会话对象。 最后,它将fake会话注入应用程序作为sut的属性。

现在,您已准备好编写测试,检查调用updateSearchResults(_ :)是否解析伪数据。 添加以下测试:

func test_UpdateSearchResults_ParsesData() {
  // given
  let promise = expectation(description: "Status code: 200")

  // when
  XCTAssertEqual(
    sut.searchResults.count, 
    0, 
    "searchResults should be empty before the data task runs")
  let url = 
    URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba")
  let dataTask = sut.defaultSession.dataTask(with: url!) {
    data, response, error in
    // if HTTP request is successful, call updateSearchResults(_:) 
    // which parses the response data into Tracks
    if let error = error {
      print(error.localizedDescription)
    } else if let httpResponse = response as? HTTPURLResponse,
      httpResponse.statusCode == 200 {
      self.sut.updateSearchResults(data)
    }
    promise.fulfill()
  }
  dataTask.resume()
  wait(for: [promise], timeout: 5)

  // then
  XCTAssertEqual(sut.searchResults.count, 3, "Didn't parse 3 items from fake response")
}

您仍然必须将其写为异步测试,因为stub是异步方法。

when断言是在数据任务运行之前searchResults为空。这应该是真的,因为你在setUp()中创建了一个全新的SUT

fake数据包含三个Track对象的JSON,因此then断言是视图控制器的searchResults数组包含三个项目。

运行测试。它应该很快成功,因为没有任何真正的网络连接!

2. Fake Update to Mock Object

上一个测试使用stub来提供伪对象的输入。接下来,您将使用mock object来测试您的代码是否正确更新了UserDefaults

重新开启BullsEye项目。该应用程序有两种游戏风格:用户可以移动滑块以匹配目标值,也可以从滑块位置猜测目标值。右下角的分段控件可切换游戏风格并将其保存在user defaults中。

您的下一个测试将检查应用程序是否正确保存了gameStyle属性。

Test navigator中,单击New Unit Test Class并将其命名为BullsEyeMockTests。在import语句下面添加以下内容:

@testable import BullsEye

class MockUserDefaults: UserDefaults {
  var gameStyleChanged = 0
  override func set(_ value: Int, forKey defaultName: String) {
    if defaultName == "gameStyle" {
      gameStyleChanged += 1
    }
  }
}

MockUserDefaults覆盖set(_:forKey :)以增加gameStyleChanged标志。 通常,您会看到设置Bool变量的类似测试,但增加Int会为您提供更大的灵活性 - 例如,您的测试可以检查该方法仅被调用一次。

BullsEyeMockTests中声明SUTmock object

var sut: ViewController!
var mockUserDefaults: MockUserDefaults!

接下来,用这个替换默认的setUp()tearDown()

override func setUp() {
  super.setUp()

  sut = UIStoryboard(name: "Main", bundle: nil)
    .instantiateInitialViewController() as? ViewController
  mockUserDefaults = MockUserDefaults(suiteName: "testing")
  sut.defaults = mockUserDefaults
}

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

这将创建SUTmock object,并将mock object注入为SUT的属性。

现在,用以下代码替换模板中的两个默认测试方法:

func testGameStyleCanBeChanged() {
  // given
  let segmentedControl = UISegmentedControl()

  // when
  XCTAssertEqual(
    mockUserDefaults.gameStyleChanged, 
    0, 
    "gameStyleChanged should be 0 before sendActions")
  segmentedControl.addTarget(sut,
    action: #selector(ViewController.chooseGameStyle(_:)), for: .valueChanged)
  segmentedControl.sendActions(for: .valueChanged)

  // then
  XCTAssertEqual(
    mockUserDefaults.gameStyleChanged, 
    1, 
    "gameStyle user default wasn't changed")
}

when断言是在测试方法更改分段控件之前gameStyleChanged标志为0。 因此,如果then断言也是真的,则意味着set(_:forKey :)只被调用一次。

运行测试;它应该成功。


UI Testing in Xcode

UI测试允许您测试与用户界面的交互。 UI测试的工作原理是通过查询应用程序的UI对象,合成事件,然后将事件发送到这些对象。 通过API,您可以检查UI对象的属性和状态,以便将它们与预期状态进行比较。

BullsEye项目的Test navigator中,添加一个新的UI Test Target。 检查要测试的目标是BullsEye,然后接受默认名称BullsEyeUITests

打开BullsEyeUITests.swift并在BullsEyeUITests类的顶部添加此属性:

var app: XCUIApplication!

setUp()中,使用以下代码替换语句XCUIApplication().launch()

app = XCUIApplication()
app.launch()

testExample()的名称更改为testGameStyleSwitch()

testGameStyleSwitch()中打开一个新行,然后单击编辑器窗口底部的红色Record按钮:

这将在模拟器中以一种模式打开应用程序,该模式将您的交互记录为测试命令。 应用加载后,点按游戏风格开关的滑动Slide部分和顶部标签。 然后,单击Xcode Record按钮停止录制。

您现在在testGameStyleSwitch()中有以下三行:

let app = XCUIApplication()
app.buttons["Slide"].tap()
app.staticTexts["Get as close as you can to: "].tap()

Recorder已创建代码来测试您在应用程序中测试的相同操作。 向sliderlabel发送一个tap。 您将使用它们作为基础来创建自己的UI测试。
如果您看到任何其他语句,只需删除它们即可。

第一行复制您在setUp()中创建的属性,因此删除该行。 你不需要点击任何东西,所以也要删除第2行和第3行末尾的.tap()。现在,打开["Slide"]旁边的小菜单,选择segmentedControls.buttons [“Slide”]

您剩下的应该是以下内容:

app.segmentedControls.buttons["Slide"]
app.staticTexts["Get as close as you can to: "]

点击任何其他对象,让recorder帮助您找到您可以在测试中访问的代码。 现在,用这段代码替换这些行来创建给定given的部分:

// given
let slideButton = app.segmentedControls.buttons["Slide"]
let typeButton = app.segmentedControls.buttons["Type"]
let slideLabel = app.staticTexts["Get as close as you can to: "]
let typeLabel = app.staticTexts["Guess where the slider is: "]

现在您已经在分段控件中有两个按钮的名称,以及两个可能的顶部标签,请在下面添加以下代码:

// then
if slideButton.isSelected {
  XCTAssertTrue(slideLabel.exists)
  XCTAssertFalse(typeLabel.exists)

  typeButton.tap()
  XCTAssertTrue(typeLabel.exists)
  XCTAssertFalse(slideLabel.exists)
} else if typeButton.isSelected {
  XCTAssertTrue(typeLabel.exists)
  XCTAssertFalse(slideLabel.exists)

  slideButton.tap()
  XCTAssertTrue(slideLabel.exists)
  XCTAssertFalse(typeLabel.exists)
}

当您在分段控件中的每个按钮上tap()时,将检查是否存在正确的标签。 运行测试 - 所有断言都应该成功。


Performance Testing

从Apple的文档Apple’s documentation中:

A performance test takes a block of code that you want to evaluate and runs it ten times, collecting the average execution time and the standard deviation for the runs. The averaging of these individual measurements form a value for the test run that can then be compared against a baseline to evaluate success or failure.

性能测试需要一段您想要评估的代码并运行十次,收集平均执行时间和运行的标准偏差。 这些单独测量的平均值形成测试运行的值,然后可以与基线进行比较以评估成功或失败。

编写性能测试非常简单:只需将要测量的代码放入measure()的闭包中即可。

要查看此操作,请重新打开HalfTunes项目,并在HalfTunesFakeTests.swift中添加以下测试:

func test_StartDownload_Performance() {
  let track = Track(
    name: "Waterloo", 
    artist: "ABBA",
    previewUrl: 
      "http://a821.phobos.apple.com/us/r30/Music/d7/ba/ce/mzm.vsyjlsff.aac.p.m4a")

  measure {
    self.sut.startDownload(track)
  }
}

运行测试,然后单击measure()尾随闭包开头旁边的图标以查看统计信息。

单击Set Baseline以设置参考时间。 然后,再次运行性能测试并查看结果 - 它可能比baseline更好或更差。 使用Edit按钮可以将基线重置为此新结果。

每个设备配置存储基线,因此您可以在几个不同的设备上执行相同的测试,并根据特定配置的处理器速度,内存等维护不同的基线。

每当您对可能影响正在测试的方法的性能的应用程序进行更改时,请再次运行性能测试以查看它与基准的比较情况。


Code Coverage

代码覆盖率工具会告诉您测试实际运行的应用程序代码,因此您知道应用程序代码的哪些部分尚未经过测试。

要启用代码覆盖,请编辑scheme’sTest操作,并选中Options选项卡下的Gather coverage复选框:

运行所有测试(Command-U),然后打开报告导航器(Command-9)。 选择该列表中顶部项目下的Coverage

单击显示三角形以查看SearchViewController.swift中的函数和闭包列表:

向下滚动到updateSearchResults(_ :)以查看覆盖率为87.9%

单击此功能的箭头按钮以打开源文件到该功能。 当您将鼠标悬停在右侧边栏中的coverage注释上时,代码部分会突出显示绿色或红色:

覆盖注释显示测试命中每个代码部分的次数;未调用的部分以红色突出显示。 正如您所期望的那样,for循环运行了3次,但错误路径中没有执行任何操作。

要增加此函数的覆盖范围,您可以复制abbaData.json,然后对其进行编辑,以便导致不同的错误。 例如,将"results"更改为"result"以进行打印测试print("Results key not found in dictionary")

1. 100% Coverage?

你有多努力争取100%的代码覆盖率? 谷歌“100%单元测试覆盖率”,你会发现一系列支持和反对的论据,以及对“100%覆盖率”的定义的争论。 反对它的争论说最后10-15%不值得努力。 支持它的争论说最后10-15%是最重要的,因为它很难测试。 谷歌“hard to unit test bad design”,以找到有说服力的论据,即不可测试的代码是更深层次设计问题的标志untestable code is a sign of deeper design problems

以下是一些可供进一步研究的资源:

后记

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

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

推荐阅读更多精彩内容