# \[iOS] UnitTest With XCode

此篇, 將利用1個簡單的project來介紹Xcode內建的Unit Test, 該project只會做簡單的幾件事情:

&#x20;(1.) 當user按下Calculate Button時計算2個輸入數字X與Y的總和, 並將結果顯示在resultLabel上.

&#x20;(2.) 當user按下Request Button時發送get request, 並將response data顯示在responseTextView上.

&#x20;(3.) 在viewWillAppear生命週期時清空resultLabel與responseTextView的文字

![](/files/-LI1YHOA6q106VkgiuYk)

**你可以在**[**這裡**](https://drive.google.com/drive/folders/1aULKZb5uy58U-0kSEMlhffvHoXXHt_lP?usp=sharing)**下載完整的project.**

此Project裡有3個主要的classes,

&#x20;(1.) ViewController

&#x20;(在viewWillAppear時呼叫clearData function將label與textView清空)

```swift
class ViewController: UIViewController {    @IBOutlet weak var xTextField: UITextField!    @IBOutlet weak var yTextField: UITextField!    @IBOutlet weak var resultLabel: UILabel!    @IBOutlet weak var urlTextField: UITextField!    @IBOutlet weak var responseTextView: UITextView!        override func viewDidLoad() {        super.viewDidLoad()        // Do any additional setup after loading the view, typically from a nib.    }​    override func viewWillAppear(_ animated: Bool) {        super.viewWillAppear(animated)        self.clearData()    }        override func viewDidAppear(_ animated: Bool) {        super.viewDidAppear(animated)            }        override func didReceiveMemoryWarning() {        super.didReceiveMemoryWarning()        // Dispose of any resources that can be recreated.    }​    func clearData(){        self.resultLabel.text = ""        self.responseTextView.text = ""    }        @IBAction func calculateButton_Touch(_ sender: Any) {        let x = Math.stringToInt(string: self.xTextField.text)        let y = Math.stringToInt(string: self.yTextField.text)        var outputString = "invalid input"                if let x = x, let y = y {            let result = Math.add(x: x, y: y)            outputString = "\(result)"        }                self.resultLabel.text = outputString    }        @IBAction func requestButton_Touch(_ sender: Any) {        if let url = urlTextField.text {            Network.dataTask(urlString: url) { (data, response, error) in                guard error == nil else {                    self.responseTextView.text = "\(error!.localizedDescription)"                    return                }                                if let data = data {                    if let jsonString = String(data: data, encoding: .utf8){                        DispatchQueue.main.async {                            self.responseTextView.text = jsonString    class ViewController: UIViewController {
    @IBOutlet weak var xTextField: UITextField!
    @IBOutlet weak var yTextField: UITextField!
    @IBOutlet weak var resultLabel: UILabel!
    @IBOutlet weak var urlTextField: UITextField!
    @IBOutlet weak var responseTextView: UITextView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }
​
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        self.clearData()
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
​
    func clearData(){
        self.resultLabel.text = ""
        self.responseTextView.text = ""
    }
    
    @IBAction func calculateButton_Touch(_ sender: Any) {
        let x = Math.stringToInt(string: self.xTextField.text)
        let y = Math.stringToInt(string: self.yTextField.text)
        var outputString = "invalid input"
        
        if let x = x, let y = y {
            let result = Math.add(x: x, y: y)
            outputString = "\(result)"
        }
        
        self.resultLabel.text = outputString
    }
    
    @IBAction func requestButton_Touch(_ sender: Any) {
        if let url = urlTextField.text {
            Network.dataTask(urlString: url) { (data, response, error) in
                guard error == nil else {
                    self.responseTextView.text = "\(error!.localizedDescription)"
                    return
                }
                
                if let data = data {
                    if let jsonString = String(data: data, encoding: .utf8){
                        DispatchQueue.main.async {
                            self.responseTextView.text = jsonString
                        }
                    }
                }
            }
        }
    }
}
```

​

&#x20;(2.) Math

&#x20;add: 將2個數字相加並回傳

&#x20;stringToInt: string轉int並回傳結果, 若失敗回傳nil

```swift
class Math: NSObject {
   
    class func add(x: Int, y: Int) -> Int {
        let result = x + y
        return result
    }
    
    class func stringToInt(string: String?) -> Int? {
        var result:Int? = nil
        
        if let string = string {
            result = Int(string)
        }
        return result
    }
}
```

(3.) Network dataTask:&#x20;

發送一個request並回傳response

```swift
class Network: NSObject {
​
    class func dataTask(urlString: String, responseHandler: @escaping (Data?, URLResponse?, Error?) -> Swift.Void) {
        let url = URL(string: urlString)
        
        if let url = url {
            let request = URLRequest(url: url)
            let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
                print("Retrieved data")
                responseHandler(data, response, error)
            }
            task.resume()
        } else {
            let myError = NSError(domain:"", code: -9999, userInfo:nil)
            responseHandler(nil, nil, myError)
        }
    }
}
```

​

## **建置Unit Test** <a href="#jian-zhi-unit-test" id="jian-zhi-unit-test"></a>

首先, 如果您的project尚未加入Unit Test, 可以從**File -> New -> Target** 新建一個Unit Test Bundle.

![File -> New -> Target](/files/-LI1Y9WYO0300R4677K1)

![](/files/-LHqLsmit2t7H6nvsW9r)

![選擇iOS Unit Testing Bundle](/files/-LI1YS22uqDYw3JDbknN)

完成後, 在XCode左側目錄欄會發現多了一個Tests的目錄, 且裡面已經幫你建立好了1個default的測試class.

![](/files/-LI1YbknOEv6Xe1JqpWK)

​

該class的預設內容為:

```swift
import XCTest
​
class TestProjectTests: 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.
        }
    }
    
}

```

我們可以注意到, Unit Test用的class必須import **XCTest**並extend **XCTestCase,**

這邊有4個function,

**setUp:** 測試class生命週期, 在**測試開始之前被呼叫**

**tearDown:** 測試class生命週期, 在**測試完成之後被呼叫**

**testExample:** 預設的test functio&#x6E;**,** 注意到所有的test function命名都必須以**tes**t開頭

**testPerformanceExample:** 預設的test function, 在**meaure** block裡面的動作皆會執行10次, 用以測試performance

​

接著, 我們也可以新增新的Test Class分別去測試我們的不同class

**MathTest**: 用來測試Math class的functions

```swift
import XCTest

class MathTest: 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 testAdd() {
        let a = Math.add(x: 3, y: 5)
        let b = Math.add(x: 64, y: -70)
        let c = Math.add(x: 999, y: 0)
        
        XCTAssert(a == 8, "a should be 8")
        XCTAssert(b == -6, "b should be -6")
        XCTAssert(c == 999, "c should be 999")
    }
    
    func testStringToInt() {
        let a = Math.stringToInt(string: "9")
        let b = Math.stringToInt(string: "-55")
        let c = Math.stringToInt(string: "sdfsks")
        
        XCTAssert(a == 9, "a should be 8")
        XCTAssert(b == -55, "b should be -55")
        XCTAssert(c == nil, "c should be nil")
    }
}
```

**​**

**TestNetwork**: 用來測試Network class的dataTask function, 關於網路測試是否成功有2個重點

&#x20;(1.) request要有發送出去

&#x20;(2.) 發送出去的url, parameters, headers是否正確

在這個例子裡面我們在response的時候檢查 response?.url?.absoluteString是否跟原本的相同.

然而, 更嚴謹的網路測試通常不會真的發送request出去, 因為這樣會影響測試時間並可能污染server端的資料, 此時就可以用**Dependency Injection(DI)**&#x7B49;方法來達到測試目的, 詳情可以參考以下連結:

​[The complete guide to Network Unit Testing in Swift](https://medium.com/flawless-app-stories/the-complete-guide-to-network-unit-testing-in-swift-db8b3ee2c327)​

```swift
import XCTest

class TestNetwork: 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 testDataTask() {
        let urlString = "http://opendata2.epa.gov.tw/UV/UV.json"

        Network.dataTask(urlString: urlString) { (data, response, error) in
            let requestUrl = response?.url?.absoluteString
            
            XCTAssert(requestUrl == urlString, "wrong URL!")
        }
    }
    
}

```

​

**TestViewController**: 用來測試Viewcontroller class, 因Viewcontroller在viewWillAppear裡面呼叫了clearData function, 所以我們在testClearData() 裡面測試相關元件的text是否為空.

**這邊需注意:**&#x20;

(1.) test class的Viewcontroller需要用**storyboard.instantiateViewController**的方式去宣告, 否則IBOutlet元件會為nil&#x20;

(2. ) 必須呼叫 vc.loadView() 才能啟動Viewcontroller的生命週期

```swift
import XCTest

class TestViewController: XCTestCase {
  
    override func setUp() {
        super.setUp()
    }
    
    override func tearDown() {
        // Put teardown code here. This method is called after the invocation of each test method in the class.
        super.tearDown()
    }
    
    func testClearData() {
        let storyboard = UIStoryboard(name: "Main", bundle: Bundle(for: ViewController.self))
        let vc = storyboard.instantiateViewController(withIdentifier: "ViewController") as! ViewController
        vc.loadView() // call life cycle
        
        vc.clearData()
        
        XCTAssert(vc.resultLabel!.text == "", "vc.resultLabel!.text should be empty ")
        XCTAssert(vc.responseTextView!.text == "", "vc.responseTextView!.text should be empty ")
    }
}

```

​

## 進行測試 <a href="#hang" id="hang"></a>

點擊Xcode的測試按鈕就能開始測試, 結束後可以在這個頁面看到完整的測試結果, 點擊每個function的綠色勾勾也可以個別測試單個function

![](/files/-LI1Yt0ODAvNlcHJbekw)

​

## 測試覆蓋率 <a href="#fu-lv" id="fu-lv"></a>

(1.) 進入**Edit Scheme -> Test -> Options**頁面將**Gather coverage for all targets**選項打勾

![](/files/-LI1YwMtqw6vO-l6BbgC)

​

(2.) 完成測試後就可以在這裡看到目前測試的coverage rate

![](/files/-LI1YzGsxbcETqDUUEGy)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://frost.gitbook.io/iosbook/ios/ios-book/ios-unittest-with-xcode.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
