swift 单元测试1

2022-07-26  本文已影响0人  f8d1cf28626a

swift 单元测试1

1、为什么要进行单元测试?

答:单元测试是为了避免你的app变成充满bug的软件,让我们在开发过程中能更好的发现缺陷,提高代码质量,也能保证在代码重构时及时发现改动带来的问题。

2、单元测试应该测什么?

1)核心功能:模型类和方法,以及它们和控制器的交互

2)最常用的UI操作

3)边际条件

4)bug修复

3、单元测试需要遵循的原则是什么?

FIRST原则--测试的最佳实践,遵循FIRST原则会让你的测试更加清晰有用

1)Fast:测试的运行速度要快,这样人们就不介意你运行它们
2)Independent/Isolated:一个测试不应当依赖于另一个测试
3)Repeatable:同一个测试,每次都应该获得相同的结果。外部数据提供者和并发问题会导致间歇性的出错
4)Self-validating:测试应当是完全自动化的,输出结果要么是pass要么是fail,而不是依靠程序员地日志文件的解释
5)Timely:理想情况下,测试的编写,应当在编写要测试的产品代码之前

4、单元测试的优点有哪些?

答:

1)使开发人员更自信
2)代码不会退化。不会因为改了bug而导致另外的bug
3)在有良好的单元测试情况下,可以放心的进行代码重构
4)良好的单元测试,本身就是使用说明,有时比文档更有用

目前很多的开源库、开源项目都加入了单元测试,例如swift版本的AFN--Alamofire,就编写了大量的测试代码,目前单元测试主要分为TDDBDD两种思维模式。

5、什么是TDD?

TDD是 Test Drive Development,指的是测试驱动开发,相对于普通思维模式来说,是一种比较极端的做法我们一般都是先编写产品代码,在写测试代码,而TDD恰好相反,其思想是先写测试代码,然后再编写相应的产品代码。

TDD一般遵循 red->green->refactor 的步骤,即(报错->通过->重构),因为是先写了测试代码,而还未添加产品代码,所以编译器会给出红色报警,当把相应的产品代码添加完善后,并让单元测试用例通过测试,通过的状态为绿色,如此反复直到各种边界和测试都进行完毕,此时我们就可以得到一个稳定的产品,所以可以大胆的对产品代码进行重构,只要保证项目最后是绿色状态,就说嘛重构的代码没问题。

TDD的过程,类似于脚本语言的交互式编程,写几行代码,就可以检查运行结果,如果结果有误,则要把最近的代码重写,知道单元测试结果正确为止。

6、什么是BDD?

BDD是Behavior Drive Development ,指的是行为驱动开发,常用于敏捷开发中使用的测试方法,其主要是为了解决XCTest苹果官方测试框架测试时难以mock和stub的问题。

BDD提倡使用Given...When...Then 这种类似自然语言的描述来编写测试代码,在objc中,现在比较流行的BDD框架有specta、Kiwi、ceder,github上start较多的是Kiwi,在swift中,专用的 BDD 测试框架是QuickSleipnir

例如Alamofire中下载的测试:

7、什么是Stub?

Stub是指人为地让一个对象对某个方法返回我们事先规定好的值。

Stub运用的主要场景是你需要和别的开发人员协同开发时,别人的模块尚未完成,而你的模块需要用到别人的模块,这时就需要Stub。例如,后端的接口未完成,你的代码已经完成了,Stub可以伪造一个调用的返回。

ojbc下可以使用OHHTTPStubs来伪造网络的数据返回。swift下,仍要手动写stub。

8、什么是Mock?

Mock是一个非常容易和stub混淆的概念,简单来说,我们可以将Mock看做是一种更全面和更智能的Stub。

明确来说,Mock其实是一个对象,它是对现有类行为的一种模拟(或是岁现有接口实现的模拟)。

Mock和Stub最大的区别在于Stub只是简单的方法替换,不涉及新的对象,被stub的对象可以是业务代码中真正的对象,而Mock行为本身产生新的(不可能在业务代码中出现)的对象,并遵循类的定义响应某些方法。

Mock让你可以检查某种情况下,一个方法是否被调用,或者一个属性是否被正确设值。objc下可以使用OCMock来mock对象。但是,由于swift的runtime比较弱,所以,swift上一般要手动写mock。

swift 单元测试2

1、XCTest框架概述

XCTest是苹果官方的测试框架,是基于OCUnit的传统测试框架,测试编写起来非常简单。

XCTest 的优缺点:

1)优点:与 Xcode 深度集成,有专门的Test 导航栏,

2)缺点:因为受限于官方测试API,因此功能不是很丰富。在书写性和可读性上都不太好。在测试用例太多的时候,由于各个测试方法是割裂的,想在某个很长的测试文件中找到特定的某个测试并搞明白这个测试是在做什么并不是很容易的事情。所有的测试都是由断言完成的,而很多时候断言的意义并不是特别的明确,对于项目交付或者新的开发人员加入时,往往要花上很大成本来进行理解或者转换。另外,每一个测试的描述都被写在断言之后,夹杂在代码之中,难以寻找。使用XCTest测试另外一个问题是难以进行mock或者stub。

2、XCTestCase概述

XCTestCase是苹果官方提供的一个单元测试工具,它的初始化不是用户控制的,开发者无需手动针对XCTestCase的subclass进行alloc和init或者调用静态方法初始化的操作。

针对一个功能模块的单元测试(针对某个class),只需要单独给这个类创建一个继承于XCTestCase,在文件中实现下面基本函数后(一般系统会默认创建这三个函数),需要测试的逻辑只需要开发者自行定义以test开头的函数,然后在那边实现自己针对某个函数、返回数值结果、操作等的测试脚本即可,按comman+u执行,函数头上出现出现蓝色的标记表示通过测试,否则直接报红色错误。

import XCTest
@testable import test
 
class testTests: XCTestCase {
 
    override func setUp() {
        super.setUp()
        // Put setup code here. This method is called before the invocation of each test method in the class.
    }
 
    override func tearDown() {
        // Put teardown code here. This method is called after the invocation of each test method in the class.
        super.tearDown()
    }
 
    func testExample() {
        // This is an example of a functional test case.
        // Use XCTAssert and related functions to verify your tests produce the correct results.
    }
 
    func testPerformanceExample() {
        // This is an example of a performance test case.
        self.measure {
            // Put the code you want to measure the time of here.
        }
    }
 
}

从注释我们可以知道这4个函数的意思

函数 用途
setUp 继承与XCTestCase 函数测试文件开始执行的时候运行
tearDown 继承与XCTestCase 测试函数运行完之后执行
testExample 测试的例子函数
testPerformanceExample 性能测试

3、使用XCTest框架进行单元测试

1)创建一个单元测试Target

单元测试target的创建方式有2种

方式一:在创建新项目时,勾选 Include Unit Tests,项目创建完成就会生成一个单元测试target,target名称默认为 项目名称+Tests

方式二:在已存在的项目中创建,按comman+5 打开xcode的测试导航器,点击左下角的 + 按钮,然后从菜单中选择 New Unit Test Target…

运行这个测试类的方法有三种:

(1)Product\Test 或者 Command-U。这实际上会运行所有测试类。

(2)点击测试导航器中的箭头按钮。

(3)点击中缝上的钻石图标

(1)基本逻辑测试处理测试

(2)异步加载数据测试

(3)数据mock测试

//断言,最基本的测试,如果expression为true则通过,否则打印后面格式化字符串
  XCTAssert(expression, format...)
 
  //Bool测试:  
  XCTAssertTrue(expression, format...)
  XCTAssertFalse(expression, format...)
 
  //相等测试
  XCTAssertEqual(expression1, expression2, format...)
  XCTAssertNotEqual(expression1, expression2, format...)
 
  //double float 对比数据测试使用
  XCTAssertEqualWithAccuracy(expression1, expression2, accuracy, format...)
  XCTAssertNotEqualWithAccuracy(expression1, expression2, accuracy, format...)
 
  //Nil测试,XCTAssert[Not]Nil断言判断给定的表达式值是否为nil
  XCTAssertNil(expression, format...)
  XCTAssertNotNil(expression, format...)
 
  //失败断言     
  XCTFail(format...)
函数 说明
testExample 全局变量f1 + f2 相加是否等于固定的数,断言是否相等
testIsPrimenumber 判断是否是素数 断言是否返回真
import XCTest
@testable import test
 
class SampleTests: XCTestCase {
    
    var f1 : Float?
    var f2 : Float?
 
    override func setUp() {
        super.setUp()
        
        //在测试方法执行前设置变量
        f1 = 10.0
        f2 = 20.0
    }
 
    override func tearDown() {
        //在测试方法执行完成后,清除变量
       super.tearDown()
    }
 
    func testExample() {
        XCTAssertTrue(f1! + f2! == 30.0)
    }
    
    //simpleTest
    func testIsPrimenumber(){
        let oddNumber = 5
        XCTAssertTrue(isPrimenumber(Double(oddNumber)))
    }
    func isPrimenumber(_ number : Double)->Bool{
        for no in 1...Int(sqrt(number)) {
            if Int(number)/no != 0{
                return true
            }
        }
        return false
    }
 
    func testPerformanceExample() {
        // This is an example of a performance test case.
        self.measure {
            // Put the code you want to measure the time of here.
        }
    }
 
}

创建一个BullsEyeGame模型类

import Foundation
 
class BullsEyeGame {
    var round = 0
    let startValue = 50
    var targetValue = 50
    var scoreRound = 0
    var scoreTotal = 0
    
    init() {
        startNewGame()
    }
    
    func startNewGame() {
        round = 0
        scoreTotal = 0
        startNewRound()
    }
    
    func startNewRound() {
        round = round + 1
        scoreRound = 0
        targetValue = 1 + (Int(arc4random()) % 100)
    }
    
    func check(guess: Int) -> Int {
        let difference = abs(targetValue - guess)
        //    let difference = guess - targetValue
        scoreRound = 100 - difference
        scoreTotal = scoreTotal + scoreRound
        
        return difference
    }
}

用XCTAssert测试BullsEyeGame模型类中一个核心功能:一个BullsEyeGame对象能够正确计算出一局游戏的得分吗?

主要步骤:

注:测试方法的名字总是以 test 开头,后面加上一个对测试内容的描述。

Given-When-Then 结构源自 BDD(行为驱动开发),是一个对客户端友好的、更少专业术语的叫法。另外也可以叫做 Arrange-Act-Assert 和 Assemble-Activate-Assert。  
 
将测试方法分成 given、when 和 then 三个部分是一种好的做法:
 
在 given 节,应该给出要计算的值:在这里,我们给出了一个猜测数,你可以指定它和 targetValue 相差多少。
 
在 when 节,执行要测试的代码,调用 gameUnderTest.check(_:)方法。
 
在 then 节,将结果和你期望的值进行断言(这里,gameUnderTest.scoreRound 应该是 100-5),如果测试失败,打印指定的消息。

编写测试代码

import XCTest
@testable import test
 
class BullsEyeTests: XCTestCase {
 
    var gameUnderTest : BullsEyeGame!
    
    override func setUp() {
        super.setUp()
        
        gameUnderTest = BullsEyeGame()
        gameUnderTest.startNewGame()
    }
 
    override func tearDown() {
        gameUnderTest = nil
        
        super.tearDown()
    }
 
    // XCTAssert to test model
    func testScoreIsComputed() {
        // 1. given
        let guess = gameUnderTest.targetValue + 5
        
        // 2. when
        _ = gameUnderTest.check(guess: guess)
        
        // 3. then
        XCTAssertEqual(gameUnderTest.scoreRound, 95, "Score computed from guess is wrong")
    }
    
    func testPerformanceExample() {
        
        self.measure {
            
        }
    }
 
}

最后:点击中缝上或者测试导航器上的钻石图标。App 会编译并运行,钻石图标会变成绿色的对勾

用 XCTestExpectation 测试异步操作

异步测试需要用到的场景:

(1)打开文档

(2)在其他线程工作

(3)和服务器或者扩展进行交流

(4)网络活动

(5)动画

(6)UI测试的一些条件

举例:网络请求异步测试

步骤:

(1)pod导入alamofire,Target是你要测试的tests Target.
(2)新建期望,用alamofire 发起请求。
(3)请求回调里断言是否为空,fullfill期望看是否满足期望
(4)XCWaiter设置期望完成的时间

编写测试代码

import XCTest
import Alamofire
 
@testable import test
 
class NetworkAsyncTests: XCTestCase {
 
    override func setUp() {
        super.setUp()
    }
 
    override func tearDown() {
        super.tearDown()
    }
 
    func testAsynNetworkTest() {
        let networkExpection = expectation(description: "networkDownSuccess")
        Alamofire.request("http://www.httpbin.org/get?key=Xctest", method: .get, parameters: nil, encoding: JSONEncoding.default).responseJSON { (respons) in
            XCTAssertNotNil(respons)
            networkExpection.fulfill()
            }
        
        //设置XCWaiter等待期望时间,只是细节不同。
//        waitForExpectations(timeout: 0.00000001)
//        wait(for: [networkExpection], timeout: 0.00000001)
 
            
        //XCTWaiter.Result  枚举类型如下
        /*
        public enum Result : Int {
            
            
            case completed
            
            case timedOut
            
            case incorrectOrder
            
            case invertedFulfillment
            
            case interrupted
        }
        */
        let result = XCTWaiter(delegate: self).wait(for: [networkExpection], timeout:  1)
        if result == .timedOut {
            print("超时")
        }
        
    }
 
    func testPerformanceExample() {
        
        self.measure {
           
        }
    }
 
}

模拟对象和交互

大部分 App 会和系统或库对象打交道——->你无法控制这些对象——->和这些对象交互时测试会变慢和不可重现,这违背了 FIRST 原则的其中两条。但是,你可以通过从存根获取数据或者写入模拟对象写入来模拟这种交互。

步骤:

(1)在 import语句后导入@testable import HalfTunes
(2)定义SUT是vc以及需要准备好预下载的数据
(3)在setup 函数中设置配置sut
(4)编写测试方法

模拟网络请求: DHURLSessionMock.swift 已经定义好了
模拟数据:https://itunes.apple.com/search?media=music&entity=song&term=abba&limit=3 下载得到一个一个 1.txt 之类的文件中。打开它,检查它的 JSON 格式,然后重命名为 abbaData.json,然后将它拖到 testSimulationObjectsTests 的文件组中

import XCTest
@testable import test
 
class testSimulationObjectsTests: XCTestCase {
    //声明SUT被测对象
    var controllerUnderTest: SearchViewController!
    
    override func setUp() {
        super.setUp()
        //构建SUT对象
        controllerUnderTest = UIStoryboard(name: "Main",
                                           bundle: nil).instantiateViewController(withIdentifier: "SearchVC") as! SearchViewController
        
        //获取下载好并拖入项目的json文件
        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)
        
        //项目中有一个 DHURLSessionMock.swift 文件。它定义了一个简单的协议 DHURLSession,包含了用 URL 或者 URLRequest 来创建 data taks 的方法。还有实现了这个协议的 URLSessionMock 类,它的初始化方法允许你用指定的数据、response 和 error 来创建一个伪造的 URLSession
        //构造模拟数据和response
        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)
        //创建一个伪造的session对象
        let sessionMock = URLSessionMock(data: data, response: urlResponse, error: nil)
        //将伪造的对象注入app的属性中
        controllerUnderTest.defaultSession = sessionMock
    }
    
    override func tearDown() {
        //释放SUT对象
        controllerUnderTest = nil
        super.tearDown()
    }
    
    // 用 DHURLSession 协议和模拟数据伪造 URLSession
    func test_UpdateSearchResults_ParsesData() {
        // given
        let promise = expectation(description: "Status code: 200")
        
        // when
        XCTAssertEqual(controllerUnderTest?.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 = controllerUnderTest?.defaultSession.dataTask(with: url!) {
            data, response, error in
            // 如果 HTTP 请求成功,调用 updateSearchResults(_:) 方法,它会将数据解析成 Tracks 对象
            if let error = error {
                print(error.localizedDescription)
            } else if let httpResponse = response as? HTTPURLResponse {
                if httpResponse.statusCode == 200 {
                    promise.fulfill()
                    self.controllerUnderTest?.updateSearchResults(data)
                }
            }
        }
        dataTask?.resume()
        waitForExpectations(timeout: 5, handler: nil)
        
        // then
        XCTAssertEqual(controllerUnderTest?.searchResults.count, 3, "Didn't parse 3 items from fake response")
    }
    
    // Performance
    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.controllerUnderTest?.startDownload(track)
        }
    }
    
    func testPerformanceExample() {
        // This is an example of a performance test case.
        self.measure {
            // Put the code you want to measure the time of here.
        }
    }
        
}

模拟写入mock对象

BullsEye有两种游戏方式:用户可以拖动 slider 来猜数字,也可以通过 slider 的位置来猜数字。右下角的 segmented 控件可以切换游戏方式,并将 gameStyle 保存到 UserDefaults。

这个测试将检查 app 是否正确地保存了 gameStyle 到 UserDefaults 里。

(1)在 BullsEyeMockTests 中声明 SULT 和 MockUserDefaults 对象
(2)在 setup() 方法中,创建 SUT 和伪造对象,然后将伪造对象注入到 SUT 的属性中
(3)在 tearDown() 中释放 SUT 和伪造对象
(4)模拟和 UserDefaults 的交互

编写测试代码

import XCTest
@testable import test
 
//MockUserDefaults 重写了 set(_:forKey:) 方法,用于增加 gameStyleChanged 的值。通常你可能认为应当使用 Bool 变量,但使用 Int 能带来更多的好处——例如,在你的测试中你可以检查这个方法是否真的被调用过一次。
class MockUserDefaults: UserDefaults {
    var gameStyleChanged = 0
    override func set(_ value: Int, forKey defaultName: String) {
        if defaultName == "gameStyle" {
            gameStyleChanged += 1
        }
    }
}
 
class BullsEyeMockTests: XCTestCase {
 
    //声明 SULT 和 MockUserDefaults 对象:
    var BullsEyeUnderTest: BullsEyeGameViewController!
    var mockUserDefaults: MockUserDefaults!
    
    override func setUp() {
        super.setUp()
        //创建 SUT 和伪造对象,然后将伪造对象注入到 SUT 的属性中
        BullsEyeUnderTest = UIStoryboard(name: "Main",
                                         bundle: nil).instantiateViewController(withIdentifier: "BullsEyeGameVC") as! BullsEyeGameViewController
        mockUserDefaults = MockUserDefaults(suiteName: "testing")!
        BullsEyeUnderTest.defaults = mockUserDefaults
    }
    
    override func tearDown() {
        //释放 SUT 和伪造对象:
        BullsEyeUnderTest = nil
        mockUserDefaults = nil
        super.tearDown()
    }
    
    // 模拟和 UserDefaults 的交互
    func testGameStyleCanBeChanged() {
        // given
        let segmentedControl = UISegmentedControl()
        
        // when
        XCTAssertEqual(mockUserDefaults.gameStyleChanged, 0, "gameStyleChanged should be 0 before sendActions")
        segmentedControl.addTarget(BullsEyeUnderTest,
                                   action: #selector(BullsEyeGameViewController.chooseGameStyle(_:)), for: .valueChanged)
        segmentedControl.sendActions(for: .valueChanged)
        
        // then
        XCTAssertEqual(mockUserDefaults.gameStyleChanged, 1, "gameStyle user default wasn't changed")
    }
    
    func testPerformanceExample() {
        // This is an example of a performance test case.
        self.measure {
            // Put the code you want to measure the time of here.
        }
    }
    
}

Code Converage工具使用

Code Converage主要是用来检测测试的覆盖率

在product->scheme->Edit Scheme

选中test ->option->勾选Code Converage

按command+u 执行测试代码,打开Xcode左边窗口的Report Navigator,找到 Project Log

选择Test可以看到一下界面

再选中coverage。可以查看代码的覆盖率,打开详情,可以点击文件进入查看测试在该文件的覆盖情况,橘黄色的代表还未执行的,绿色代表执行的,右边的次数代表执行的次数


上一篇下一篇

猜你喜欢

热点阅读