美文网首页Swift学习
自动化Test使用详细解析(二) —— 单元测试和UI Test

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

作者: 刀客传奇 | 来源:发表于2019-04-19 14:41 被阅读95次

    版本记录

    版本号 时间
    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使用简单示例,感兴趣的给个赞或者关注~~~

    相关文章

      网友评论

        本文标题:自动化Test使用详细解析(二) —— 单元测试和UI Test

        本文链接:https://www.haomeiwen.com/subject/gghpwqtx.html