iOS开发—单元测试和UI测试教程(下)

发表于:2022-5-19 08:51

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:小小小_小朋友    来源:稀土掘金

  快速失败
  失败是痛苦的,但它不必永远持续下去。
  要体验失败,只需将 URL 更改testValidApiCallGetsHTTPStatusCode200()为无效的 URL:
  让url =  URL(字符串:“http://www.randomnumberapi.com/test”)!
 
  运行测试。它失败了,但它需要完整的超时间隔!这是因为您假设请求总是会成功,这就是您调用promise.fulfill().?由于请求失败,它仅在超时到期时才完成。
  您可以通过更改假设来改进这一点并让测试更快地失败。与其等待请求成功,不如等待异步方法的完成处理程序被调用。一旦应用程序收到来自服务器的响应(OK 或错误),就会发生这种情况,这满足了预期。然后您的测试可以检查请求是否成功。
  要查看其工作原理,请创建一个新测试。
  但首先,通过撤消您对url.
  然后,将以下测试添加到您的类中:
  func  testApiCallCompletes () throws {
     // 给定
    let urlString =  "http://www.randomnumberapi.com/test" 
    let url =  URL (string: urlString) ! 
    让promise =期望(描述:“调用完成处理程序”)
     var statusCode:Int?
    var responseError:错误?
    // 当
    let dataTask = sut.dataTask(with: url) { _ , response, error in 
      statusCode = (response as?  HTTPURLResponse ) ? .statusCode
      响应错误=错误
      promise.fulfill()
    }
    数据任务.resume()
    等待(为:[承诺],超时:5)
    // 然后
    XCTAssertNil (responseError)
     XCTAssertEqual (statusCode, 200 )
  }

  关键区别在于,只需输入完成处理程序即可满足预期,而这只需要大约一秒钟的时间。如果请求失败,则then断言失败。
  运行测试。现在应该大约需要一秒钟才能失败。它失败是因为请求失败,而不是因为测试运行超出timeout。
  修复url然后再次运行测试以确认它现在成功。
  有条件地失败
  在某些情况下,执行测试没有多大意义。例如,在testValidApiCallGetsHTTPStatusCode200()没有网络连接的情况下运行会发生什么?当然,它不应该通过,因为它不会收到 200 状态码。但它也不应该失败,因为它没有测试任何东西。
  幸运的是,Apple 引入XCTSkip了在先决条件失败时跳过测试。在 的声明下方添加以下行sut:
  让networkMonitor =  NetworkMonitor .shared

  NetworkMonitorwraps?NWPathMonitor,提供了一种方便的方式来检查网络连接。
  在中,在测试的开头testValidApiCallGetsHTTPStatusCode200()添加:XCTSkipUnless
  试试 XCTSkipUnless (
    networkMonitor.isReachable,
    “此测试需要网络连接。” )

  XCTSkipUnless(_:_:)当没有网络可达时跳过测试。通过禁用网络连接并运行测试来检查这一点。您将在测试旁边的装订线中看到一个新图标,表示该测试既没有通过也没有失败。
  再次启用您的网络连接并重新运行测试以确保它在正常情况下仍然成功。将相同的代码添加到testApiCallCompletes().
  伪造对象和交互
  异步测试让您确信您的代码会为异步 API 生成正确的输入。您可能还想测试您的代码在接收来自 的输入时是否正常工作URLSession,或者它是否正确更新了UserDefaults数据库或 iCloud 容器。
  大多数应用程序与系统或库对象交互 - 您无法控制的对象。与这些对象交互的测试可能很慢且不可重复,违反了FIRST原则中的两个。相反,您可以通过从存根获取输入或更新模拟对象来伪造交互。
  当您的代码依赖于系统或库对象时,请使用伪造。通过创建一个假对象来扮演该角色并将这个假对象注入到您的代码中来做到这一点。Jon Reid 的Dependency Injection描述了几种方法来做到这一点。
  从存根伪造输入
  现在,检查应用程序getRandomNumber(completion:)是否正确解析了会话下载的数据。您将BullsEyeGame使用存根数据伪造会话。
  转到 Test navigator,单击*+并选择New Unit Test Class*?...。将其命名为BullsEyeFakeTests,将其保存在BullsEyeTests目录中并将目标设置为BullsEyeTests。
  import在语句下方导入 BullsEye 应用程序模块:
  @testable 导入BullsEye
 
  现在,将 的内容替换为BullsEyeFakeTests:
  var sut: BullsEyeGame!
  覆盖 func  setUpWithError () throws {
     try  super .setUpWithError()
    sut =  BullsEyeGame ()
  }
  覆盖 func  tearDownWithError ()抛出{
    sut =  nil
    尝试 超级.tearDownWithError()
  }

  这声明了 SUT,即在BullsEyeGame中创建它并在 中setUpWithError()释放它tearDownWithError()。
  BullsEye 项目包含支持文件URLSessionStub.swift。这定义了一个名为 的简单协议,URLSessionProtocol其中包含一个创建数据任务的方法URL。它还定义了URLSessionStub, 符合此协议。它的初始化程序允许您定义数据任务应返回的数据、响应和错误。
  要设置伪造,请转到BullsEyeFakeTests.swift并添加一个新测试:
  func  testStartNewRoundUsesRandomValueFromApiRequest () {
     // 给定
    // 1 
    let stubbedData =  "[1]" .data(using: .utf8)
     let urlString =  
      "http://www.randomnumberapi.com/api/v1.0/random?min =0&max=100&count=1"
    让url =  URL (string: urlString) ! 
    让stubbedResponse =  HTTPURLResponse (
      网址:网址,
      状态码:200,
      http版本:无,
      headerFields: nil )
    让urlSessionStub =  URLSessionStub (
      数据:存根数据,
      响应:存根响应,
      错误:无)
    sut.urlSession = urlSessionStub
    让promise =期望(描述:“收到的价值”)
    // 什么时候
    sut.startNewRound {
      // 然后
      // 2 
      XCTAssertEqual ( self .sut.targetValue, 1 )
      promise.fulfill()
    }
    等待(为:[承诺],超时:5)
  }

  这个测试做了两件事:
  1、您设置假数据和响应并创建假会话对象。最后,将假会话作为sut.
  2、您仍然必须将其编写为异步测试,因为存根伪装成异步方法。通过与存根的假号码进行比较,检查调用是否startNewRound(completion:)解析了假数据。targetValue
  运行测试。它应该很快就会成功,因为没有任何真正的网络连接!
  伪造模拟对象的更新
  之前的测试使用存根来提供来自假对象的输入。接下来,您将使用一个模拟对象来测试您的代码是否正确更新UserDefaults。
  这个应用程序有两种游戏风格。用户可以:
  1. 移动滑块以匹配目标值。
  2. 从滑块位置猜测目标值。
  右下角的分段控件切换游戏风格并将其保存为UserDefaults.
  您的下一个测试检查应用程序是否正确保存了该gameStyle属性。
  向目标BullsEyeTests添加一个新的测试类并将其命名为BullsEyeMockTests。import在语句下面添加以下内容:
  @testable 导入BullsEye
  类 MockUserDefaults : UserDefaults {
     var gameStyleChanged =  0
    覆盖 函数 集( _value  : Int , forKey defaultName : String ) {
       if defaultName == " gameStyle " {  
        游戏风格改变+=  1
      }
    }
  }

  MockUserDefaults覆盖set(_:forKey:)为增量gameStyleChanged。类似的测试通常会设置一个Bool变量,但递增Int为您提供了更大的灵活性。例如,您的测试可以检查应用程序是否只调用该方法一次。
  接下来,在BullsEyeMockTests中声明 SUT 和模拟对象:
  var sut:视图控制器!
  var mockUserDefaults: MockUserDefaults!

  替换setUpWithError()和tearDownWithError():
  覆盖 func  setUpWithError () throws {
     try  super .setUpWithError()
    sut =  UIStoryboard(名称:“Main”,捆绑:nil)
      .instantiateInitialViewController()作为? ViewController 
    mockUserDefaults =  MockUserDefaults (suiteName: "testing" )
    sut.defaults = mockUserDefaults
  }
  覆盖 func  tearDownWithError ()抛出{
    sut =  nil 
    mockUserDefaults =  nil 
    try  super .tearDownWithError()
  }

  这将创建 SUT 和模拟对象,并将模拟对象作为 SUT 的属性注入。
  现在,将模板中的两个默认测试方法替换为:
  func  testGameStyleCanBeChanged () {
     // 给定
    let segmentedControl =  UISegmentedControl ()
    // 当
    XCTAssertEqual (
      mockUserDefaults.gameStyleChanged,
      0 , 
       "gameStyleChanged 在 sendActions 之前应该为 0" )
    分段控制.addTarget(
      苏,
      动作:#selector ( ViewController.chooseGameStyle ( _ :)),
      对于:.valueChanged)
    segmentedControl.sendActions(for: .valueChanged)
    // 然后
    XCTAssertEqual (
      mockUserDefaults.gameStyleChanged,
      1、 
       “gameStyle用户默认没有改变”)
  }

  when断言是在测试方法改变分段控制之前gameStyleChanged标志为0 。因此,如果then断言也为真,则意味着set(_:forKey:)只调用了一次。
  运行测试。它应该成功。
  Xcode 中的 UI 测试
  UI 测试允许您测试与用户界面的交互。UI 测试的工作原理是通过查询查找应用程序的 UI 对象,合成事件,然后将事件发送到这些对象。该 API 使您能够检查 UI 对象的属性和状态,以将它们与预期状态进行比较。
  在测试导航器中,添加一个新的UI 测试目标。检查要测试的目标是BullsEye,然后接受默认名称BullsEyeUITests。
  打开BullsEyeUITests.swift并在类的顶部添加这个属性BullsEyeUITests:
  var应用程序:XCUIApplication!

  删除tearDownWithError()并替换setUpWithError()以下内容:
  尝试 超级.setUpWithError()
  continueAfterFailure =  false 
  app =  XCUIApplication ()
  app.launch()
 
  删除两个现有测试并添加一个名为testGameStyleSwitch().
  func  testGameStyleSwitch () {    
  }

  在其中打开一个新行,然后单击编辑器窗口底部的testGameStyleSwitch()红色记录按钮:
  这将以将您的交互记录为测试命令的模式在模拟器中打开应用程序。应用加载后,点击游戏风格开关的Slide部分和顶部标签。再次单击 Xcode?Record按钮以停止录制。
  您现在有以下三行testGameStyleSwitch():
  让app =  XCUIApplication ()
  app.buttons[ “幻灯片” ].tap()
  app.staticTexts[ "尽可能靠近:" ].tap()

  记录器已创建代码来测试您在应用程序中测试的相同操作。轻按一下游戏风格的分段控件和顶部标签。您将使用这些作为基础来创建您自己的 UI 测试。如果您看到任何其他陈述,只需将其删除。
  第一行复制了您在 中创建的属性setUpWithError(),因此删除该行。你还不需要点击任何东西,所以也要.tap()在第 2 行和第 3 行的末尾删除。现在,打开旁边的小菜单["Slide"]并选择segmentedControls.buttons["Slide"]。
  你应该留下:
  app.segmentedControls.buttons[ “幻灯片” ]
  app.staticTexts[ "尽可能靠近:" ]

  点击任何其他对象,让记录器帮助您找到可以在测试中访问的代码。现在,用此代码替换这些行以创建给定部分:
  // 给定
  let slideButton = app.segmentedControls.buttons[ "Slide" ]
   let typeButton = app.segmentedControls.buttons[ "Type" ]
   let slideLabel = app.staticTexts[ "尽可能接近:" ]
   let typeLabel = app.staticTexts[ "猜猜滑块在哪里:" ]

  现在您已经有了分段控件中两个按钮的名称和两个可能的顶部标签,请在下面添加以下代码:
  // 然后
  如果slideButton.isSelected {
     XCTAssertTrue (slideLabel.exists)
     XCTAssertFalse (typeLabel.exists)
    类型按钮.tap()
    XCTAssertTrue (typeLabel.exists)
     XCTAssertFalse (slideLabel.exists)
  } else  if typeButton.isSelected {
     XCTAssertTrue (typeLabel.exists)
     XCTAssertFalse (slideLabel.exists)
    滑动按钮.tap()
    XCTAssertTrue (slideLabel.exists)
     XCTAssertFalse (typeLabel.exists)
  }

  tap()这将检查您在分段控件中的每个按钮上是否存在正确的标签。运行测试——所有断言都应该成功。
  测试性能
  来自苹果的文档:
  性能测试获取您想要评估的代码块并运行十次,收集平均执行时间和运行的标准偏差。这些单独测量的平均值形成了测试运行的值,然后可以将其与基线进行比较以评估成功或失败。
  编写性能测试很简单:只需将要测量的代码放入measure().?此外,您可以指定要衡量的多个指标。
  将以下测试添加到BullsEyeTests:
  func  testScoreIsComputedPerformance () {
    措施(
      指标:[
        XCTClockMetric (), 
         XCTCPUMetric (),
         XCTStorageMetric (), 
         XCTMemoryMetric ()
      ]
    ) {
      sut.check(猜测:100)
    }
  }

  该测试测量多个指标:
  ·XCTClockMetric测量经过的时间。
  · XCTCPUMetric跟踪 CPU 活动,包括 CPU 时间、周期和指令数。
  · XCTStorageMetric告诉您测试代码写入存储的数据量。
  · XCTMemoryMetric跟踪使用的物理内存量。
  measure()运行测试,然后单击出现在尾随闭包开头旁边的图标以查看统计信息。您可以更改 Metric 旁边的选定指标。
  单击设置基线以设置参考时间。再次运行性能测试并查看结果——它可能比基线更好或更差。编辑按钮允许您将基线重置为这个新结果。
  基线是按设备配置存储的,因此您可以在多个不同的设备上执行相同的测试。每个都可以根据特定配置的处理器速度、内存等保持不同的基线。
  每当您对可能影响被测试方法性能的应用程序进行更改时,请再次运行性能测试以查看它与基线的比较情况。
  启用代码覆盖率
  代码覆盖率工具会告诉您测试实际运行的应用程序代码,因此您知道应用程序的哪些部分没有经过测试——至少目前还没有。
  要启用代码覆盖率,请编辑方案的测试操作并选中选项选项卡下的**收集覆盖率复选框:
  使用Command-U运行所有测试,然后使用Command-9打开报告导航器。在该列表的顶部项目下选择Coverage :
  单击显示三角形以查看BullsEyeGame.swift中的函数和闭包列表:
  滚动getRandomNumber(completion:)查看覆盖率为 95.0%。
  单击此函数的箭头按钮以打开该函数的源文件。当您将鼠标悬停在右侧边栏中的覆盖注释上时,代码部分会突出显示绿色或红色:
  覆盖注释显示测试命中每个代码部分的次数。未调用的部分以红色突出显示。
  实现 100% 的覆盖率?
  你应该多努力争取 100% 的代码覆盖率?只需谷歌“100% 单元测试覆盖率”,您就会发现支持和反对这一点的一系列论据,以及关于“100% 覆盖率”定义的争论。反对它的论点说最后 10%–15% 不值得努力。它的论据说最后 10%–15% 是最重要的,因为它很难测试。谷歌“难以对糟糕的设计进行单元测试”以找到有说服力的论点,即不可测试的代码是更深层次设计问题的标志。

  本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理
21天更文挑战,赢取价值500元大礼,还有机会成为签约作者!

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计

法律顾问:上海漕溪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2022, 沪ICP备05003035号
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪公网安备 31010102002173号